From f11f4def438d296b80dbab5932a1054b21b90519 Mon Sep 17 00:00:00 2001 From: Michael McGuinness Date: Fri, 28 Feb 2025 13:11:53 +0000 Subject: [PATCH] Merged in feature/clean (pull request #76) Clean Set Up * fail * starteddb * constraint * cleantest * fixqueries * fixtests * storemimetype * buffer * tests * setup * passingtests * pdfHElloWorld * rm * test * notodos * tests * clean * testpdf --- .gitignore | 3 +- api/docCleanRunner/runner_test.go | 118 +- api/docTextRunner/runner_test.go | 9 +- cmd/docCleanRunner/main.go | 8 + .../00000000000006_documents.up.sql | 15 +- .../00000000000102_job_views.up.sql | 6 +- database/queries/clean.sql | 10 +- database/queries/job.sql | 25 +- go.mod | 4 + go.sum | 8 + internal/database/repository/clean.sql.go | 30 +- internal/database/repository/clean_test.go | 54 +- internal/database/repository/job.sql.go | 50 +- internal/database/repository/job_test.go | 43 +- internal/database/repository/models.go | 122 +- internal/database/repository/result_test.go | 38 +- internal/database/repository/text_test.go | 8 +- internal/document/clean/clean.go | 102 +- internal/document/clean/clean_test.go | 187 +- internal/document/clean/content.go | 52 + internal/document/clean/contentType.go | 110 + internal/document/clean/contentType_test.go | 459 +++ internal/document/clean/content_test.go | 59 + internal/document/clean/create.go | 2 +- internal/document/clean/create_test.go | 62 +- internal/document/clean/pdf.go | 54 + internal/document/clean/pdf_test.go | 43 + internal/document/clean/service.go | 2 + internal/document/clean/service_test.go | 2 + internal/document/clean/version/version.go | 1 - internal/document/text/create_test.go | 9 +- internal/document/text/extract.go | 8 +- internal/document/text/extract_test.go | 9 +- internal/document/text/version/version.go | 1 - internal/query/types/contextFull/creator.go | 2 - internal/query/types/contextFull/result.go | 1 - internal/query/types/contextFull/service.go | 1 - internal/query/types/contextFull/updator.go | 2 - internal/query/types/jsonExtractor/creator.go | 2 +- internal/query/types/jsonExtractor/updator.go | 1 - internal/serviceconfig/queue/delete.go | 2 +- scripts/local-deployments.yml | 20 +- test/process_test.go | 66 +- vendor/github.com/hhrutter/lzw/.gitignore | 6 + vendor/github.com/hhrutter/lzw/LICENSE | 27 + vendor/github.com/hhrutter/lzw/README.md | 37 + vendor/github.com/hhrutter/lzw/reader.go | 238 ++ vendor/github.com/hhrutter/lzw/writer.go | 283 ++ vendor/github.com/hhrutter/tiff/.gitignore | 6 + vendor/github.com/hhrutter/tiff/LICENSE | 27 + vendor/github.com/hhrutter/tiff/README.md | 23 + vendor/github.com/hhrutter/tiff/buffer.go | 69 + vendor/github.com/hhrutter/tiff/compress.go | 58 + vendor/github.com/hhrutter/tiff/consts.go | 149 + vendor/github.com/hhrutter/tiff/reader.go | 735 ++++ vendor/github.com/hhrutter/tiff/writer.go | 482 +++ vendor/github.com/pdfcpu/pdfcpu/LICENSE.txt | 202 ++ .../internal/corefont/metrics/metrics.go | 55 + .../internal/corefont/metrics/standard.go | 678 ++++ .../pdfcpu/pdfcpu/pkg/filter/ascii85Decode.go | 89 + .../pdfcpu/pkg/filter/asciiHexDecode.go | 86 + .../pdfcpu/pdfcpu/pkg/filter/ccittDecode.go | 101 + .../pdfcpu/pdfcpu/pkg/filter/dctDecode.go | 56 + .../pdfcpu/pdfcpu/pkg/filter/filter.go | 125 + .../pdfcpu/pdfcpu/pkg/filter/flateDecode.go | 361 ++ .../pdfcpu/pdfcpu/pkg/filter/lzwDecode.go | 100 + .../pdfcpu/pdfcpu/pkg/filter/paeth.go | 75 + .../pdfcpu/pkg/filter/runLengthDecode.go | 153 + .../pdfcpu/pdfcpu/pkg/font/install.go | 1040 ++++++ .../pdfcpu/pdfcpu/pkg/font/metrics.go | 426 +++ .../github.com/pdfcpu/pdfcpu/pkg/log/log.go | 279 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/annotation.go | 1030 ++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/booklet.go | 609 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/bookmark.go | 633 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/color/color.go | 176 + .../pdfcpu/pkg/pdfcpu/createAnnotations.go | 1220 +++++++ .../pdfcpu/pkg/pdfcpu/createRenditions.go | 340 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/createTestPDF.go | 1990 +++++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/crypto.go | 1822 ++++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/cut.go | 650 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/doc.go | 53 + .../pdfcpu/pdfcpu/pkg/pdfcpu/draw/draw.go | 197 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/extract.go | 702 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/font/fontDict.go | 1107 ++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/format/format.go | 71 + .../pdfcpu/pdfcpu/pkg/pdfcpu/iccProfile.go | 311 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/image.go | 392 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/importImage.go | 382 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/info.go | 610 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/keyword.go | 143 + .../pdfcpu/pdfcpu/pkg/pdfcpu/matrix/matrix.go | 95 + .../pdfcpu/pdfcpu/pkg/pdfcpu/merge.go | 993 ++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/migrate.go | 291 ++ .../pdfcpu/pkg/pdfcpu/model/annotation.go | 1674 +++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/attach.go | 429 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/booklet.go | 236 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/box.go | 1257 +++++++ .../pdfcpu/pkg/pdfcpu/model/colorSpace.go | 32 + .../pdfcpu/pkg/pdfcpu/model/configuration.go | 544 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/context.go | 648 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/cut.go | 225 ++ .../pdfcpu/pkg/pdfcpu/model/dereference.go | 470 +++ .../pdfcpu/pkg/pdfcpu/model/destination.go | 81 + .../pdfcpu/pkg/pdfcpu/model/document.go | 884 +++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/equal.go | 239 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/image.go | 627 ++++ .../pdfcpu/pkg/pdfcpu/model/metadata.go | 155 + .../pdfcpu/pkg/pdfcpu/model/nameTree.go | 616 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/nup.go | 374 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/page.go | 106 + .../pdfcpu/pdfcpu/pkg/pdfcpu/model/parse.go | 1297 +++++++ .../pdfcpu/pkg/pdfcpu/model/parseConfig.go | 153 + .../pdfcpu/pkg/pdfcpu/model/parseConfig_js.go | 321 ++ .../pdfcpu/pkg/pdfcpu/model/parseContent.go | 411 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/repair.go | 56 + .../pdfcpu/pdfcpu/pkg/pdfcpu/model/resize.go | 192 ++ .../pdfcpu/pkg/pdfcpu/model/resource.go | 178 + .../pdfcpu/model/resources/Roboto-Regular.ttf | Bin 0 -> 171272 bytes .../pkg/pdfcpu/model/resources/config.yml | 70 + .../pdfcpu/pdfcpu/pkg/pdfcpu/model/stat.go | 142 + .../pdfcpu/pdfcpu/pkg/pdfcpu/model/text.go | 778 +++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/version.go | 109 + .../pdfcpu/pkg/pdfcpu/model/watermark.go | 461 +++ .../pdfcpu/pkg/pdfcpu/model/xreftable.go | 2815 +++++++++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/model/zoom.go | 147 + .../pdfcpu/pdfcpu/pkg/pdfcpu/nup.go | 833 +++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/optimize.go | 1554 +++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/page.go | 263 ++ .../pdfcpu/pkg/pdfcpu/primitives/band.go | 228 ++ .../pdfcpu/pkg/pdfcpu/primitives/bar.go | 97 + .../pdfcpu/pkg/pdfcpu/primitives/border.go | 87 + .../pdfcpu/pkg/pdfcpu/primitives/buttons.go | 171 + .../pdfcpu/pkg/pdfcpu/primitives/checkBox.go | 777 +++++ .../pdfcpu/pkg/pdfcpu/primitives/comboBox.go | 814 +++++ .../pdfcpu/pkg/pdfcpu/primitives/content.go | 1320 +++++++ .../pdfcpu/pkg/pdfcpu/primitives/date.go | 100 + .../pdfcpu/pkg/pdfcpu/primitives/dateField.go | 950 ++++++ .../pdfcpu/pkg/pdfcpu/primitives/divider.go | 63 + .../pkg/pdfcpu/primitives/fieldGroup.go | 404 +++ .../pdfcpu/pkg/pdfcpu/primitives/font.go | 340 ++ .../pdfcpu/pkg/pdfcpu/primitives/guide.go | 46 + .../pdfcpu/pkg/pdfcpu/primitives/imageBox.go | 692 ++++ .../pdfcpu/pkg/pdfcpu/primitives/listBox.go | 1018 ++++++ .../pdfcpu/pkg/pdfcpu/primitives/margin.go | 79 + .../pdfcpu/pkg/pdfcpu/primitives/padding.go | 79 + .../pdfcpu/pkg/pdfcpu/primitives/page.go | 374 ++ .../pdfcpu/pkg/pdfcpu/primitives/pdf.go | 1135 ++++++ .../pkg/pdfcpu/primitives/radioButtonGroup.go | 1088 ++++++ .../pdfcpu/pkg/pdfcpu/primitives/regions.go | 162 + .../pdfcpu/pkg/pdfcpu/primitives/simpleBox.go | 295 ++ .../pdfcpu/pkg/pdfcpu/primitives/table.go | 963 ++++++ .../pdfcpu/pkg/pdfcpu/primitives/textBox.go | 464 +++ .../pdfcpu/pkg/pdfcpu/primitives/textField.go | 1077 ++++++ .../pkg/pdfcpu/primitives/textFieldLabel.go | 88 + .../pdfcpu/pdfcpu/pkg/pdfcpu/property.go | 97 + .../pdfcpu/pdfcpu/pkg/pdfcpu/read.go | 3031 +++++++++++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/resize.go | 253 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/rotate.go | 54 + .../pdfcpu/pdfcpu/pkg/pdfcpu/scan/scan.go | 65 + .../pdfcpu/pdfcpu/pkg/pdfcpu/stamp.go | 2186 ++++++++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/anchor.go | 73 + .../pdfcpu/pdfcpu/pkg/pdfcpu/types/array.go | 196 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/date.go | 429 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/dict.go | 540 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/layout.go | 411 +++ .../pdfcpu/pkg/pdfcpu/types/paperSize.go | 208 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/slice.go | 59 + .../pdfcpu/pkg/pdfcpu/types/streamdict.go | 470 +++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/string.go | 306 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/types.go | 624 ++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/types/utf16.go | 176 + .../pdfcpu/pdfcpu/pkg/pdfcpu/write.go | 1085 ++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/writeImage.go | 994 ++++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/writeObjects.go | 833 +++++ .../pdfcpu/pdfcpu/pkg/pdfcpu/writePages.go | 297 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/writeStats.go | 264 ++ .../pdfcpu/pdfcpu/pkg/pdfcpu/zoom.go | 186 + vendor/golang.org/x/image/LICENSE | 27 + vendor/golang.org/x/image/PATENTS | 22 + vendor/golang.org/x/image/ccitt/reader.go | 795 +++++ vendor/golang.org/x/image/ccitt/table.go | 972 ++++++ vendor/golang.org/x/image/ccitt/writer.go | 102 + vendor/golang.org/x/image/riff/riff.go | 193 ++ vendor/golang.org/x/image/vp8/decode.go | 403 +++ vendor/golang.org/x/image/vp8/filter.go | 273 ++ vendor/golang.org/x/image/vp8/idct.go | 98 + vendor/golang.org/x/image/vp8/partition.go | 129 + vendor/golang.org/x/image/vp8/pred.go | 201 ++ vendor/golang.org/x/image/vp8/predfunc.go | 553 +++ vendor/golang.org/x/image/vp8/quant.go | 98 + vendor/golang.org/x/image/vp8/reconstruct.go | 442 +++ vendor/golang.org/x/image/vp8/token.go | 381 +++ vendor/golang.org/x/image/vp8l/decode.go | 603 ++++ vendor/golang.org/x/image/vp8l/huffman.go | 245 ++ vendor/golang.org/x/image/vp8l/transform.go | 299 ++ vendor/golang.org/x/image/webp/decode.go | 276 ++ vendor/golang.org/x/image/webp/doc.go | 9 + vendor/modules.txt | 29 + 198 files changed, 71722 insertions(+), 146 deletions(-) create mode 100644 internal/document/clean/content.go create mode 100644 internal/document/clean/contentType.go create mode 100644 internal/document/clean/contentType_test.go create mode 100644 internal/document/clean/content_test.go create mode 100644 internal/document/clean/pdf.go create mode 100644 internal/document/clean/pdf_test.go create mode 100644 vendor/github.com/hhrutter/lzw/.gitignore create mode 100644 vendor/github.com/hhrutter/lzw/LICENSE create mode 100644 vendor/github.com/hhrutter/lzw/README.md create mode 100644 vendor/github.com/hhrutter/lzw/reader.go create mode 100644 vendor/github.com/hhrutter/lzw/writer.go create mode 100644 vendor/github.com/hhrutter/tiff/.gitignore create mode 100644 vendor/github.com/hhrutter/tiff/LICENSE create mode 100644 vendor/github.com/hhrutter/tiff/README.md create mode 100644 vendor/github.com/hhrutter/tiff/buffer.go create mode 100644 vendor/github.com/hhrutter/tiff/compress.go create mode 100644 vendor/github.com/hhrutter/tiff/consts.go create mode 100644 vendor/github.com/hhrutter/tiff/reader.go create mode 100644 vendor/github.com/hhrutter/tiff/writer.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/LICENSE.txt create mode 100644 vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/metrics.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/standard.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ascii85Decode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/asciiHexDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ccittDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/dctDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/filter.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/flateDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/lzwDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/paeth.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/filter/runLengthDecode.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/font/install.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/font/metrics.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/log/log.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/annotation.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/booklet.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/bookmark.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color/color.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createAnnotations.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createRenditions.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createTestPDF.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/crypto.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/cut.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/doc.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw/draw.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/extract.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font/fontDict.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format/format.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/iccProfile.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/image.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/importImage.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/info.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/keyword.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix/matrix.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/merge.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/migrate.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/annotation.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/attach.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/booklet.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/box.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/colorSpace.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/configuration.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/context.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/cut.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/dereference.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/destination.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/document.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/equal.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/image.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/metadata.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nameTree.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nup.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/page.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parse.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig_js.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseContent.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/repair.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resize.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resource.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/Roboto-Regular.ttf create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/config.yml create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/stat.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/text.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/version.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/watermark.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/xreftable.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/zoom.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/nup.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/optimize.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/page.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/band.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/bar.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/border.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/buttons.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/checkBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/comboBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/content.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/date.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/dateField.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/divider.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/fieldGroup.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/font.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/guide.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/imageBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/listBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/margin.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/padding.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/page.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/pdf.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/radioButtonGroup.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/regions.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/simpleBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/table.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textBox.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textField.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textFieldLabel.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/property.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/read.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/resize.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/rotate.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan/scan.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/stamp.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/anchor.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/array.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/date.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/dict.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/layout.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/paperSize.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/slice.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/streamdict.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/string.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/types.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/utf16.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/write.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeImage.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeObjects.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writePages.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeStats.go create mode 100644 vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/zoom.go create mode 100644 vendor/golang.org/x/image/LICENSE create mode 100644 vendor/golang.org/x/image/PATENTS create mode 100644 vendor/golang.org/x/image/ccitt/reader.go create mode 100644 vendor/golang.org/x/image/ccitt/table.go create mode 100644 vendor/golang.org/x/image/ccitt/writer.go create mode 100644 vendor/golang.org/x/image/riff/riff.go create mode 100644 vendor/golang.org/x/image/vp8/decode.go create mode 100644 vendor/golang.org/x/image/vp8/filter.go create mode 100644 vendor/golang.org/x/image/vp8/idct.go create mode 100644 vendor/golang.org/x/image/vp8/partition.go create mode 100644 vendor/golang.org/x/image/vp8/pred.go create mode 100644 vendor/golang.org/x/image/vp8/predfunc.go create mode 100644 vendor/golang.org/x/image/vp8/quant.go create mode 100644 vendor/golang.org/x/image/vp8/reconstruct.go create mode 100644 vendor/golang.org/x/image/vp8/token.go create mode 100644 vendor/golang.org/x/image/vp8l/decode.go create mode 100644 vendor/golang.org/x/image/vp8l/huffman.go create mode 100644 vendor/golang.org/x/image/vp8l/transform.go create mode 100644 vendor/golang.org/x/image/webp/decode.go create mode 100644 vendor/golang.org/x/image/webp/doc.go diff --git a/.gitignore b/.gitignore index 8aef8be5..760c6e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ **/*.http **/*.cast +assets/ # Ignore the binary files generated by `go build` /main @@ -74,4 +75,4 @@ Thumbs.db *.wmv # Devbox -.devbox/ \ No newline at end of file +.devbox/ diff --git a/api/docCleanRunner/runner_test.go b/api/docCleanRunner/runner_test.go index 54c53a4e..d72006b6 100644 --- a/api/docCleanRunner/runner_test.go +++ b/api/docCleanRunner/runner_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" doccleanrunner "queryorchestration/api/docCleanRunner" "queryorchestration/internal/database" "queryorchestration/internal/database/repository" @@ -15,8 +16,10 @@ import ( "queryorchestration/internal/serviceconfig/queue/documenttext" objectstoremock "queryorchestration/mocks/objectstore" queuemock "queryorchestration/mocks/queue" + "strings" "testing" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/aws/aws-sdk-go-v2/service/sqs/types" "github.com/go-playground/validator/v10" @@ -56,30 +59,46 @@ func TestDocCleanRunner(t *testing.T) { }) assert.NotNil(t, runner) - doc := doccleanrunner.Body{ + bod := doccleanrunner.Body{ ID: uuid.New(), } - bodyBytes, err := json.Marshal(doc) + bodyBytes, err := json.Marshal(bod) assert.NoError(t, err) body := string(bodyBytes) msg := &types.Message{ Body: &body, } + doc := document.Document{ + ID: bod.ID, + JobID: uuid.New(), + Hash: "example_hash", + } inloc := document.Location{ Bucket: "bucket_name", Key: "/i/am/here", } + dbid := database.MustToDBUUID(doc.ID) - pool.ExpectQuery("name: IsDocumentClean :one").WithArgs(database.MustToDBUUID(doc.ID)).WillReturnRows( + pool.ExpectQuery("name: HasDocumentCleanEntry :one").WithArgs(dbid).WillReturnRows( pgxmock.NewRows([]string{"isclean"}). AddRow(false), ) - pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(database.MustToDBUUID(doc.ID)).WillReturnRows( + pool.ExpectQuery("name: GetDocument :one").WithArgs(dbid). + WillReturnRows( + pgxmock.NewRows([]string{"id", "jobId", "hash"}). + AddRow(dbid, database.MustToDBUUID(doc.JobID), doc.Hash), + ) + pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(dbid).WillReturnRows( pgxmock.NewRows([]string{"documentId", "bucket", "key"}). - AddRow(database.MustToDBUUID(doc.ID), inloc.Bucket, inloc.Key), + AddRow(dbid, inloc.Bucket, inloc.Key), ) - pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(database.MustToDBUUID(doc.ID), int32(1), inloc.Bucket, inloc.Key). + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } + pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(dbid, int32(1), &inloc.Bucket, &inloc.Key, dbmimetype, repository.NullCleanfailtype{}). WillReturnResult(pgxmock.NewResult("", 1)) mockSQS.EXPECT(). @@ -92,6 +111,93 @@ func TestDocCleanRunner(t *testing.T) { ). Return(&sqs.SendMessageOutput{}, nil) + mockStore.EXPECT(). + HeadObject( + mock.Anything, + mock.MatchedBy(func(in *s3.HeadObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.HeadObjectOutput{ + ContentType: &mimeType, + }, nil) + + bodyMsg := io.NopCloser(strings.NewReader(pdfHelloWorld)) + mockStore.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: bodyMsg, + }, nil) + err = runner.Process(ctx, msg) assert.NoError(t, err) } + +const pdfHelloWorld = `%PDF-1.4 +%���� +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R + /Version /1.4 +>> +endobj +2 0 obj +<< + /Type /Pages + /Kids [3 0 R] + /Count 1 +>> +endobj +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Resources << + /Font << + /F1 << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica + >> + >> + >> + /Contents 4 0 R +>> +endobj +4 0 obj +<< + /Length + 44 +>> +stream +BT +/F1 24 Tf +100 700 Td +(Hello World!) Tj +ET +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000086 00000 n +0000000151 00000 n +0000000376 00000 n +trailer +<< + /Size 5 + /Root 1 0 R +>> +startxref +472 +%%EOF` diff --git a/api/docTextRunner/runner_test.go b/api/docTextRunner/runner_test.go index 5274a923..40465755 100644 --- a/api/docTextRunner/runner_test.go +++ b/api/docTextRunner/runner_test.go @@ -71,9 +71,14 @@ func TestDocCleanRunner(t *testing.T) { AddRow(false), ) cleanId := database.MustToDBUUID(uuid.New()) + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } pool.ExpectQuery("name: GetDocumentCleanEntry :one").WithArgs(database.MustToDBUUID(doc.ID)).WillReturnRows( - pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version"}). - AddRow(cleanId, database.MustToDBUUID(doc.ID), inloc.Bucket, inloc.Key, int32(1)), + pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "mimetype", "fail"}). + AddRow(cleanId, database.MustToDBUUID(doc.ID), &inloc.Bucket, &inloc.Key, int32(1), dbmimetype, repository.NullCleanfailtype{}), ) pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(int32(1), inloc.Bucket, inloc.Key, cleanId). WillReturnResult(pgxmock.NewResult("", 1)) diff --git a/cmd/docCleanRunner/main.go b/cmd/docCleanRunner/main.go index 4c80aa7e..a836bbdd 100644 --- a/cmd/docCleanRunner/main.go +++ b/cmd/docCleanRunner/main.go @@ -8,6 +8,7 @@ import ( documentclean "queryorchestration/internal/document/clean" cleanversion "queryorchestration/internal/document/clean/version" "queryorchestration/internal/server/runner" + "queryorchestration/internal/serviceconfig/objectstore" "queryorchestration/internal/serviceconfig/queue/documenttext" _ "github.com/lib/pq" @@ -16,6 +17,7 @@ import ( type DocCleanConfig struct { runner.BaseConfig documenttext.DocTextConfig + objectstore.ObjectStoreConfig } func main() { @@ -39,5 +41,11 @@ func main() { os.Exit(1) } + err = cfg.SetStoreClient(ctx) + if err != nil { + slog.Error(err.Error()) + os.Exit(1) + } + server.Listen(ctx) } diff --git a/database/migrations/00000000000006_documents.up.sql b/database/migrations/00000000000006_documents.up.sql index 7d7c1a91..8cb1339f 100644 --- a/database/migrations/00000000000006_documents.up.sql +++ b/database/migrations/00000000000006_documents.up.sql @@ -14,13 +14,22 @@ CREATE TABLE documentEntries ( foreign key (documentId) references documents(id) ); +CREATE TYPE cleanFailType AS ENUM ('invalid_mimetype'); +CREATE TYPE cleanMimeTypes AS ENUM ('application/pdf'); + CREATE TABLE documentCleans ( id uuid primary key DEFAULT uuid_generate_v7(), documentId uuid not null, version int not null, - bucket text not null, - key text not null, - foreign key (documentId) references documents(id) + bucket text, + key text, + mimetype cleanMimeTypes, + fail cleanFailType, + foreign key (documentId) references documents(id), + CONSTRAINT bucket_and_key_together CHECK ((bucket IS NULL) = (key IS NULL) and (bucket is null) = (mimetype is null)), + CONSTRAINT location_xor_fail CHECK ( + (bucket IS NOT NULL) != (fail IS NOT NULL) + ) ); CREATE TABLE documentTextExtractions ( diff --git a/database/migrations/00000000000102_job_views.up.sql b/database/migrations/00000000000102_job_views.up.sql index 52081b91..2baa48ac 100644 --- a/database/migrations/00000000000102_job_views.up.sql +++ b/database/migrations/00000000000102_job_views.up.sql @@ -27,6 +27,8 @@ WITH RankedExtractions AS ( dc.bucket, dc.key, dc.version, + dc.mimetype, + dc.fail, ROW_NUMBER() OVER (PARTITION BY dc.documentId ORDER BY dc.id DESC) as row_num FROM documents d JOIN currentCollectorMinCleanVersions ccv ON d.jobId = ccv.jobId @@ -38,7 +40,9 @@ SELECT documentId, bucket, key, - version + version, + mimetype, + fail FROM RankedExtractions WHERE row_num = 1; diff --git a/database/queries/clean.sql b/database/queries/clean.sql index 445ac191..a47c6d54 100644 --- a/database/queries/clean.sql +++ b/database/queries/clean.sql @@ -1,12 +1,12 @@ --- name: IsDocumentClean :one +-- name: HasDocumentCleanEntry :one SELECT EXISTS( - SELECT 1 FROM currentCleanEntries WHERE documentId = $1 + SELECT 1 FROM currentCleanEntries WHERE documentId = @documentId ); -- name: AddDocumentCleanEntry :exec -INSERT INTO documentCleans (documentId, version, bucket, key) VALUES ($1, $2, $3, $4); +INSERT INTO documentCleans (documentId, version, bucket, key, mimetype, fail) VALUES ($1, $2, $3, $4, $5, $6); -- name: GetDocumentCleanEntry :one -SELECT id, documentId, bucket, key, version +SELECT id, documentId, bucket, key, version, mimetype, fail FROM currentCleanEntries - WHERE documentId = $1; \ No newline at end of file + WHERE documentId = @documentId; diff --git a/database/queries/job.sql b/database/queries/job.sql index 8333edd0..f754c2b2 100644 --- a/database/queries/job.sql +++ b/database/queries/job.sql @@ -19,26 +19,37 @@ docs AS ( -- Get all documents for this job SELECT id, jobId FROM documents WHERE jobId = $1 ), +doc_clean_entries as ( + -- Documents with their current clean entries + SELECT + d.id AS document_id, + d.jobId as job_id, + cte.id AS clean_entry_id + FROM + docs d + LEFT JOIN currentCleanEntries cte ON cte.documentId = d.id + WHERE cte.id is null or cte.id is not null and cte.fail is null +), doc_text_entries AS ( -- Documents with their current text entries SELECT - d.id AS document_id, + d.document_id, cte.id AS text_entry_id FROM - docs d - LEFT JOIN currentTextEntries cte ON cte.documentId = d.id + doc_clean_entries d + LEFT JOIN currentTextEntries cte ON cte.cleanEntryId = d.clean_entry_id ), required_results AS ( -- All required document-query-version combinations SELECT - d.id AS document_id, + d.document_id, cqdt.queryId, cqdt.queryVersion, dte.text_entry_id FROM - docs d - JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.jobId - JOIN doc_text_entries dte ON dte.document_id = d.id + doc_clean_entries d + JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.job_id + JOIN doc_text_entries dte ON dte.document_id = d.document_id and dte.text_entry_id IS NOT NULL ), existing_results AS ( diff --git a/go.mod b/go.mod index c315a978..c59f7757 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( github.com/labstack/echo/v4 v4.13.3 github.com/lib/pq v1.10.9 github.com/oapi-codegen/runtime v1.1.1 + github.com/pdfcpu/pdfcpu v0.9.1 github.com/prometheus/client_golang v1.20.5 github.com/stretchr/testify v1.10.0 github.com/swaggo/echo-swagger v1.4.1 @@ -47,6 +48,8 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/tiff v1.0.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -77,6 +80,7 @@ require ( go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect + golang.org/x/image v0.21.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/time v0.10.0 // indirect golang.org/x/tools v0.29.0 // indirect diff --git a/go.sum b/go.sum index 75b8a391..631e56f0 100644 --- a/go.sum +++ b/go.sum @@ -130,6 +130,10 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= +github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= @@ -203,6 +207,8 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pashagolub/pgxmock/v3 v3.4.0 h1:87VMr2q7m2+6VzXo4Tsp9kMklGlj6mMN19Hp/bp2Rwo= github.com/pashagolub/pgxmock/v3 v3.4.0/go.mod h1:FvCl7xqPbLLI3XohihJ1NzXnikjM3q/NWSixg4t9hrU= +github.com/pdfcpu/pdfcpu v0.9.1 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao= +github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -297,6 +303,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= diff --git a/internal/database/repository/clean.sql.go b/internal/database/repository/clean.sql.go index fcafc3b4..53a022c6 100644 --- a/internal/database/repository/clean.sql.go +++ b/internal/database/repository/clean.sql.go @@ -12,38 +12,42 @@ import ( ) const addDocumentCleanEntry = `-- name: AddDocumentCleanEntry :exec -INSERT INTO documentCleans (documentId, version, bucket, key) VALUES ($1, $2, $3, $4) +INSERT INTO documentCleans (documentId, version, bucket, key, mimetype, fail) VALUES ($1, $2, $3, $4, $5, $6) ` type AddDocumentCleanEntryParams struct { - Documentid pgtype.UUID `db:"documentid"` - Version int32 `db:"version"` - Bucket string `db:"bucket"` - Key string `db:"key"` + Documentid pgtype.UUID `db:"documentid"` + Version int32 `db:"version"` + Bucket *string `db:"bucket"` + Key *string `db:"key"` + Mimetype NullCleanmimetypes `db:"mimetype"` + Fail NullCleanfailtype `db:"fail"` } // AddDocumentCleanEntry // -// INSERT INTO documentCleans (documentId, version, bucket, key) VALUES ($1, $2, $3, $4) +// INSERT INTO documentCleans (documentId, version, bucket, key, mimetype, fail) VALUES ($1, $2, $3, $4, $5, $6) func (q *Queries) AddDocumentCleanEntry(ctx context.Context, arg *AddDocumentCleanEntryParams) error { _, err := q.db.Exec(ctx, addDocumentCleanEntry, arg.Documentid, arg.Version, arg.Bucket, arg.Key, + arg.Mimetype, + arg.Fail, ) return err } const getDocumentCleanEntry = `-- name: GetDocumentCleanEntry :one -SELECT id, documentId, bucket, key, version +SELECT id, documentId, bucket, key, version, mimetype, fail FROM currentCleanEntries WHERE documentId = $1 ` // GetDocumentCleanEntry // -// SELECT id, documentId, bucket, key, version +// SELECT id, documentId, bucket, key, version, mimetype, fail // FROM currentCleanEntries // WHERE documentId = $1 func (q *Queries) GetDocumentCleanEntry(ctx context.Context, documentid pgtype.UUID) (*Currentcleanentry, error) { @@ -55,23 +59,25 @@ func (q *Queries) GetDocumentCleanEntry(ctx context.Context, documentid pgtype.U &i.Bucket, &i.Key, &i.Version, + &i.Mimetype, + &i.Fail, ) return &i, err } -const isDocumentClean = `-- name: IsDocumentClean :one +const hasDocumentCleanEntry = `-- name: HasDocumentCleanEntry :one SELECT EXISTS( SELECT 1 FROM currentCleanEntries WHERE documentId = $1 ) ` -// IsDocumentClean +// HasDocumentCleanEntry // // SELECT EXISTS( // SELECT 1 FROM currentCleanEntries WHERE documentId = $1 // ) -func (q *Queries) IsDocumentClean(ctx context.Context, documentid pgtype.UUID) (bool, error) { - row := q.db.QueryRow(ctx, isDocumentClean, documentid) +func (q *Queries) HasDocumentCleanEntry(ctx context.Context, documentid pgtype.UUID) (bool, error) { + row := q.db.QueryRow(ctx, hasDocumentCleanEntry, documentid) var exists bool err := row.Scan(&exists) return exists, err diff --git a/internal/database/repository/clean_test.go b/internal/database/repository/clean_test.go index 315d0b72..a61c63ef 100644 --- a/internal/database/repository/clean_test.go +++ b/internal/database/repository/clean_test.go @@ -43,7 +43,7 @@ func TestClean(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, id) - isclean, err := queries.IsDocumentClean(ctx, id) + isclean, err := queries.HasDocumentCleanEntry(ctx, id) assert.NoError(t, err) assert.False(t, isclean) @@ -52,24 +52,66 @@ func TestClean(t *testing.T) { bucket := "example_bucket" key := "example_key" + mimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + } + fail := repository.NullCleanfailtype{ + Valid: true, + Cleanfailtype: repository.CleanfailtypeInvalidMimetype, + } err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: id, Version: 1, - Bucket: bucket, - Key: key, + Bucket: &bucket, + Key: &key, + Mimetype: mimetype, + Fail: fail, + }) + assert.Error(t, err) + + err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ + Documentid: id, + Version: 1, + Fail: fail, }) assert.NoError(t, err) - isclean, err = queries.IsDocumentClean(ctx, id) + isclean, err = queries.HasDocumentCleanEntry(ctx, id) assert.NoError(t, err) assert.True(t, isclean) clean, err := queries.GetDocumentCleanEntry(ctx, id) assert.NoError(t, err) assert.Equal(t, id, clean.Documentid) - assert.Equal(t, bucket, clean.Bucket) - assert.Equal(t, key, clean.Key) + assert.Nil(t, clean.Bucket) + assert.Nil(t, clean.Key) + assert.Equal(t, fail, clean.Fail) assert.Equal(t, int32(1), clean.Version) assert.NotEqual(t, pgtype.UUID{}, clean.ID) assert.True(t, clean.ID.Valid) + + err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ + Documentid: id, + Version: 2, + Bucket: &bucket, + Key: &key, + Mimetype: mimetype, + }) + assert.NoError(t, err) + + isclean, err = queries.HasDocumentCleanEntry(ctx, id) + assert.NoError(t, err) + assert.True(t, isclean) + + clean, err = queries.GetDocumentCleanEntry(ctx, id) + assert.NoError(t, err) + assert.Equal(t, id, clean.Documentid) + assert.Equal(t, bucket, *clean.Bucket) + assert.Equal(t, key, *clean.Key) + assert.Equal(t, mimetype, clean.Mimetype) + assert.False(t, clean.Fail.Valid) + assert.Equal(t, int32(2), clean.Version) + assert.NotEqual(t, pgtype.UUID{}, clean.ID) + assert.True(t, clean.ID.Valid) } diff --git a/internal/database/repository/job.sql.go b/internal/database/repository/job.sql.go index 1eaada5d..92d15f57 100644 --- a/internal/database/repository/job.sql.go +++ b/internal/database/repository/job.sql.go @@ -74,26 +74,37 @@ docs AS ( -- Get all documents for this job SELECT id, jobId FROM documents WHERE jobId = $1 ), +doc_clean_entries as ( + -- Documents with their current clean entries + SELECT + d.id AS document_id, + d.jobId as job_id, + cte.id AS clean_entry_id + FROM + docs d + LEFT JOIN currentCleanEntries cte ON cte.documentId = d.id + WHERE cte.id is null or cte.id is not null and cte.fail is null +), doc_text_entries AS ( -- Documents with their current text entries SELECT - d.id AS document_id, + d.document_id, cte.id AS text_entry_id FROM - docs d - LEFT JOIN currentTextEntries cte ON cte.documentId = d.id + doc_clean_entries d + LEFT JOIN currentTextEntries cte ON cte.cleanEntryId = d.clean_entry_id ), required_results AS ( -- All required document-query-version combinations SELECT - d.id AS document_id, + d.document_id, cqdt.queryId, cqdt.queryVersion, dte.text_entry_id FROM - docs d - JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.jobId - JOIN doc_text_entries dte ON dte.document_id = d.id + doc_clean_entries d + JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.job_id + JOIN doc_text_entries dte ON dte.document_id = d.document_id and dte.text_entry_id IS NOT NULL ), existing_results AS ( @@ -152,26 +163,37 @@ SELECT ( // -- Get all documents for this job // SELECT id, jobId FROM documents WHERE jobId = $1 // ), +// doc_clean_entries as ( +// -- Documents with their current clean entries +// SELECT +// d.id AS document_id, +// d.jobId as job_id, +// cte.id AS clean_entry_id +// FROM +// docs d +// LEFT JOIN currentCleanEntries cte ON cte.documentId = d.id +// WHERE cte.id is null or cte.id is not null and cte.fail is null +// ), // doc_text_entries AS ( // -- Documents with their current text entries // SELECT -// d.id AS document_id, +// d.document_id, // cte.id AS text_entry_id // FROM -// docs d -// LEFT JOIN currentTextEntries cte ON cte.documentId = d.id +// doc_clean_entries d +// LEFT JOIN currentTextEntries cte ON cte.cleanEntryId = d.clean_entry_id // ), // required_results AS ( // -- All required document-query-version combinations // SELECT -// d.id AS document_id, +// d.document_id, // cqdt.queryId, // cqdt.queryVersion, // dte.text_entry_id // FROM -// docs d -// JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.jobId -// JOIN doc_text_entries dte ON dte.document_id = d.id +// doc_clean_entries d +// JOIN collectorQueryDependencyTree cqdt ON cqdt.jobId = d.job_id +// JOIN doc_text_entries dte ON dte.document_id = d.document_id // and dte.text_entry_id IS NOT NULL // ), // existing_results AS ( diff --git a/internal/database/repository/job_test.go b/internal/database/repository/job_test.go index 2fb5dd3f..7cffec03 100644 --- a/internal/database/repository/job_test.go +++ b/internal/database/repository/job_test.go @@ -231,6 +231,32 @@ func TestJobSync(t *testing.T) { assert.NoError(t, err) assert.True(t, isSynced) + documentNoCleanID, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{ + Jobid: jobId, + Hash: "example_noclean", + }) + assert.NoError(t, err) + + isSynced, err = queries.IsJobSynced(ctx, jobId) + assert.NoError(t, err) + assert.False(t, isSynced) + + bucket := "example_bucket" + key := "example_key" + err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ + Documentid: documentNoCleanID, + Version: 1, + Fail: repository.NullCleanfailtype{ + Valid: true, + Cleanfailtype: repository.CleanfailtypeInvalidMimetype, + }, + }) + assert.NoError(t, err) + + isSynced, err = queries.IsJobSynced(ctx, jobId) + assert.NoError(t, err) + assert.True(t, isSynced) + documentID, err := queries.CreateDocument(ctx, &repository.CreateDocumentParams{ Jobid: jobId, Hash: "example_hash", @@ -241,11 +267,16 @@ func TestJobSync(t *testing.T) { assert.NoError(t, err) assert.False(t, isSynced) + keytwo := "example_key_2" err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &keytwo, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) @@ -403,8 +434,12 @@ func TestJobSync(t *testing.T) { err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) diff --git a/internal/database/repository/models.go b/internal/database/repository/models.go index fc503074..d142d344 100644 --- a/internal/database/repository/models.go +++ b/internal/database/repository/models.go @@ -11,6 +11,104 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +type Cleanfailtype string + +const ( + CleanfailtypeInvalidMimetype Cleanfailtype = "invalid_mimetype" +) + +func (e *Cleanfailtype) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Cleanfailtype(s) + case string: + *e = Cleanfailtype(s) + default: + return fmt.Errorf("unsupported scan type for Cleanfailtype: %T", src) + } + return nil +} + +type NullCleanfailtype struct { + Cleanfailtype Cleanfailtype + Valid bool // Valid is true if Cleanfailtype is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullCleanfailtype) Scan(value interface{}) error { + if value == nil { + ns.Cleanfailtype, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Cleanfailtype.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullCleanfailtype) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.Cleanfailtype), nil +} + +func (e Cleanfailtype) Valid() bool { + switch e { + case CleanfailtypeInvalidMimetype: + return true + } + return false +} + +type Cleanmimetypes string + +const ( + CleanmimetypesApplicationPdf Cleanmimetypes = "application/pdf" +) + +func (e *Cleanmimetypes) Scan(src interface{}) error { + switch s := src.(type) { + case []byte: + *e = Cleanmimetypes(s) + case string: + *e = Cleanmimetypes(s) + default: + return fmt.Errorf("unsupported scan type for Cleanmimetypes: %T", src) + } + return nil +} + +type NullCleanmimetypes struct { + Cleanmimetypes Cleanmimetypes + Valid bool // Valid is true if Cleanmimetypes is not NULL +} + +// Scan implements the Scanner interface. +func (ns *NullCleanmimetypes) Scan(value interface{}) error { + if value == nil { + ns.Cleanmimetypes, ns.Valid = "", false + return nil + } + ns.Valid = true + return ns.Cleanmimetypes.Scan(value) +} + +// Value implements the driver Valuer interface. +func (ns NullCleanmimetypes) Value() (driver.Value, error) { + if !ns.Valid { + return nil, nil + } + return string(ns.Cleanmimetypes), nil +} + +func (e Cleanmimetypes) Valid() bool { + switch e { + case CleanmimetypesApplicationPdf: + return true + } + return false +} + type Querytype string const ( @@ -129,11 +227,13 @@ type Collectorversion struct { } type Currentcleanentry struct { - ID pgtype.UUID `db:"id"` - Documentid pgtype.UUID `db:"documentid"` - Bucket string `db:"bucket"` - Key string `db:"key"` - Version int32 `db:"version"` + ID pgtype.UUID `db:"id"` + Documentid pgtype.UUID `db:"documentid"` + Bucket *string `db:"bucket"` + Key *string `db:"key"` + Version int32 `db:"version"` + Mimetype NullCleanmimetypes `db:"mimetype"` + Fail NullCleanfailtype `db:"fail"` } type Currentclientcansync struct { @@ -183,11 +283,13 @@ type Document struct { } type Documentclean struct { - ID pgtype.UUID `db:"id"` - Documentid pgtype.UUID `db:"documentid"` - Version int32 `db:"version"` - Bucket string `db:"bucket"` - Key string `db:"key"` + ID pgtype.UUID `db:"id"` + Documentid pgtype.UUID `db:"documentid"` + Version int32 `db:"version"` + Bucket *string `db:"bucket"` + Key *string `db:"key"` + Mimetype NullCleanmimetypes `db:"mimetype"` + Fail NullCleanfailtype `db:"fail"` } type Documententry struct { diff --git a/internal/database/repository/result_test.go b/internal/database/repository/result_test.go index 0eb4c508..4b95a43f 100644 --- a/internal/database/repository/result_test.go +++ b/internal/database/repository/result_test.go @@ -71,11 +71,17 @@ func TestResults(t *testing.T) { assert.NoError(t, err) assert.False(t, issynced) + bucket := "example_bucket" + key := "example_key" err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) @@ -227,11 +233,17 @@ func TestResultValues(t *testing.T) { }) assert.NoError(t, err) + bucket := "example_bucket" + key := "example_key" err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) cleanentry, err := queries.GetDocumentCleanEntry(ctx, documentID) @@ -419,11 +431,17 @@ func TestUnsyncedNoDepsQueries(t *testing.T) { assert.NoError(t, err) assert.Len(t, qs, 0) + bucket := "example_bucket" + key := "example_key" err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) @@ -490,8 +508,12 @@ func TestUnsyncedNoDepsQueries(t *testing.T) { err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: documentTwoID, Version: 1, - Bucket: "hi", - Key: "hello", + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) diff --git a/internal/database/repository/text_test.go b/internal/database/repository/text_test.go index 7660685f..6712ec54 100644 --- a/internal/database/repository/text_test.go +++ b/internal/database/repository/text_test.go @@ -48,8 +48,12 @@ func TestTextExtraction(t *testing.T) { err = queries.AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ Documentid: id, Version: 1, - Bucket: bucket, - Key: key, + Bucket: &bucket, + Key: &key, + Mimetype: repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.CleanmimetypesApplicationPdf, + }, }) assert.NoError(t, err) diff --git a/internal/document/clean/clean.go b/internal/document/clean/clean.go index 938ab73f..71b59c61 100644 --- a/internal/document/clean/clean.go +++ b/internal/document/clean/clean.go @@ -2,36 +2,100 @@ package documentclean import ( "context" + "fmt" "log/slog" "queryorchestration/internal/database" "queryorchestration/internal/database/repository" "queryorchestration/internal/document" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/google/uuid" ) type CleanParams struct { ID uuid.UUID + Hash string Location document.Location } -func (s *Service) executeCleanTasks(params *CleanParams) (*document.Location, error) { - // TODO - various cleaning tasks - return ¶ms.Location, nil +type InvalidDocumentReason string + +const ( + InvalidDocumentMimeType InvalidDocumentReason = "invalid_mimetype" + InvalidDocumentRead InvalidDocumentReason = "invalid_read" + InvalidDocumentReadPages InvalidDocumentReason = "invalid_read_pages" + InvalidDocumentZeroPageCount InvalidDocumentReason = "zero_page_count" + InvalidDocumentLargePageCount InvalidDocumentReason = "large_page_count" + InvalidDocumentLargeFile InvalidDocumentReason = "large_file" + InvalidDocumentQuality InvalidDocumentReason = "invalid_quality" +) + +type ExecuteCleanResponse struct { + location *document.Location + mimetype *MimeType + failReason *InvalidDocumentReason +} + +func (s *Service) executeCleanTasks(ctx context.Context, params *CleanParams) (*ExecuteCleanResponse, error) { + out, err := s.cfg.GetStoreClient().HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: ¶ms.Location.Bucket, + Key: ¶ms.Location.Key, + IfMatch: ¶ms.Hash, + }) + if err != nil { + return nil, err + } + + length := int64(1) + if out.ContentLength != nil { + length = *out.ContentLength + } + + mimeType, err := s.getAcceptedMimeType(ctx, params, out.ContentType, length) + if err != nil { + return nil, err + } else if mimeType == MimeTypeInvalid { + reason := InvalidDocumentMimeType + return &ExecuteCleanResponse{ + failReason: &reason, + }, nil + } + + content, err := s.getContent(ctx, params, mimeType) + if err != nil { + return nil, err + } + + corruptReason := content.isCorrupted(ctx) + if corruptReason != nil { + return &ExecuteCleanResponse{ + failReason: corruptReason, + }, nil + } + + return &ExecuteCleanResponse{ + location: ¶ms.Location, + mimetype: &mimeType, + }, nil } func (s *Service) clean(ctx context.Context, id uuid.UUID) error { slog.Debug("cleaning document", "id", id.String()) - docId := database.MustToDBUUID(id) + doc, err := s.cfg.GetDBQueries().GetDocument(ctx, docId) + if err != nil { + return err + } + entry, err := s.cfg.GetDBQueries().GetDocumentEntry(ctx, docId) if err != nil { return err } - outLocation, err := s.executeCleanTasks(&CleanParams{ - ID: id, + out, err := s.executeCleanTasks(ctx, &CleanParams{ + ID: id, + Hash: doc.Hash, Location: document.Location{ Bucket: entry.Bucket, Key: entry.Key, @@ -43,15 +107,33 @@ func (s *Service) clean(ctx context.Context, id uuid.UUID) error { version := s.svc.Version.GetVersion() - err = s.cfg.GetDBQueries().AddDocumentCleanEntry(ctx, &repository.AddDocumentCleanEntryParams{ + params := &repository.AddDocumentCleanEntryParams{ Documentid: docId, Version: version, - Bucket: outLocation.Bucket, - Key: outLocation.Key, - }) + } + if out.failReason != nil { + slog.Info("Failed document", "id", id, "reason", *out.failReason) + params.Fail = repository.NullCleanfailtype{ + Cleanfailtype: repository.Cleanfailtype(*out.failReason), + Valid: true, + } + } else { + params.Bucket = &out.location.Bucket + params.Key = &out.location.Key + params.Mimetype = repository.NullCleanmimetypes{ + Cleanmimetypes: repository.Cleanmimetypes(*out.mimetype), + Valid: true, + } + } + + err = s.cfg.GetDBQueries().AddDocumentCleanEntry(ctx, params) if err != nil { return err } + if out.failReason != nil { + return fmt.Errorf("%s", *out.failReason) + } + return nil } diff --git a/internal/document/clean/clean_test.go b/internal/document/clean/clean_test.go index 0c12ddf7..48ddd2f8 100644 --- a/internal/document/clean/clean_test.go +++ b/internal/document/clean/clean_test.go @@ -2,15 +2,20 @@ package documentclean import ( "context" + "io" "queryorchestration/internal/database" "queryorchestration/internal/database/repository" "queryorchestration/internal/document" cleanversion "queryorchestration/internal/document/clean/version" + objectstoremock "queryorchestration/mocks/objectstore" + "strings" "testing" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/google/uuid" "github.com/pashagolub/pgxmock/v3" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) func TestClean(t *testing.T) { @@ -23,6 +28,8 @@ func TestClean(t *testing.T) { cfg := &DocCleanConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 svc := Service{ cfg: cfg, @@ -31,33 +38,177 @@ func TestClean(t *testing.T) { }, } + doc := document.Document{ + ID: uuid.New(), + JobID: uuid.New(), + Hash: "example_hash", + } + inloc := document.Location{ + Bucket: "bucket_name", + Key: "/i/am/here", + } + dbid := database.MustToDBUUID(doc.ID) + + pool.ExpectQuery("name: GetDocument :one").WithArgs(dbid). + WillReturnRows( + pgxmock.NewRows([]string{"id", "jobId", "hash"}). + AddRow(dbid, database.MustToDBUUID(doc.JobID), doc.Hash), + ) + pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(dbid).WillReturnRows( + pgxmock.NewRows([]string{"documentId", "bucket", "key"}). + AddRow(dbid, inloc.Bucket, inloc.Key), + ) + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } + pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(dbid, int32(1), &inloc.Bucket, &inloc.Key, dbmimetype, repository.NullCleanfailtype{}). + WillReturnResult(pgxmock.NewResult("", 1)) + + var length int64 = 10 + mockS3.EXPECT(). + HeadObject( + mock.Anything, + mock.MatchedBy(func(in *s3.HeadObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.HeadObjectOutput{ + ContentType: &mimeType, + ContentLength: &length, + }, nil) + + body := io.NopCloser(strings.NewReader(pdfHelloWorld)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + err = svc.clean(ctx, doc.ID) + assert.NoError(t, err) +} + +func TestExecuteCleanTasks(t *testing.T) { + ctx := context.Background() + + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + id := uuid.New() + hash := "example_hash" inloc := document.Location{ Bucket: "bucket_name", Key: "/i/am/here", } - pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(database.MustToDBUUID(id)).WillReturnRows( - pgxmock.NewRows([]string{"documentId", "bucket", "key"}). - AddRow(database.MustToDBUUID(id), inloc.Bucket, inloc.Key), - ) - pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(database.MustToDBUUID(id), int32(1), inloc.Bucket, inloc.Key). - WillReturnResult(pgxmock.NewResult("", 1)) + mimeType := "application/pdf" + mockS3.EXPECT(). + HeadObject( + mock.Anything, + mock.MatchedBy(func(in *s3.HeadObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.HeadObjectOutput{ + ContentType: &mimeType, + }, nil) - err = svc.clean(ctx, id) - assert.NoError(t, err) -} + body := io.NopCloser(strings.NewReader(pdfHelloWorld)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) -func TestExecuteCleanTasks(t *testing.T) { - svc := Service{} - - id := uuid.New() - location := document.Location{} - - outloc, err := svc.executeCleanTasks(&CleanParams{ + outloc, err := svc.executeCleanTasks(ctx, &CleanParams{ ID: id, - Location: location, + Hash: hash, + Location: inloc, }) assert.NoError(t, err) - assert.EqualExportedValues(t, location, *outloc) + assert.EqualExportedValues(t, ExecuteCleanResponse{ + location: &inloc, + }, *outloc) } + +const pdfHelloWorld = `%PDF-1.4 +%���� +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R + /Version /1.4 +>> +endobj +2 0 obj +<< + /Type /Pages + /Kids [3 0 R] + /Count 1 +>> +endobj +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Resources << + /Font << + /F1 << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica + >> + >> + >> + /Contents 4 0 R +>> +endobj +4 0 obj +<< + /Length + 44 +>> +stream +BT +/F1 24 Tf +100 700 Td +(Hello World!) Tj +ET +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000086 00000 n +0000000151 00000 n +0000000376 00000 n +trailer +<< + /Size 5 + /Root 1 0 R +>> +startxref +472 +%%EOF` diff --git a/internal/document/clean/content.go b/internal/document/clean/content.go new file mode 100644 index 00000000..8fac3e57 --- /dev/null +++ b/internal/document/clean/content.go @@ -0,0 +1,52 @@ +package documentclean + +import ( + "bytes" + "context" + "errors" + "io" + + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type Content interface { + isCorrupted(context.Context) *InvalidDocumentReason +} + +func (s *Service) getContent(ctx context.Context, params *CleanParams, mimeType MimeType) (Content, error) { + resp, err := s.cfg.GetStoreClient().GetObject(ctx, &s3.GetObjectInput{ + Bucket: ¶ms.Location.Bucket, + Key: ¶ms.Location.Key, + IfMatch: ¶ms.Hash, + }) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + seek, err := ReaderToSeeker(resp.Body) + if err != nil { + return nil, err + } + + var content Content + switch mimeType { + case MimeTypePDF: + content = NewPDF(seek) + default: + return nil, errors.New("invalid mimetype") + } + + return content, nil +} + +func ReaderToSeeker(readCloser io.ReadCloser) (io.ReadSeeker, error) { + data, err := io.ReadAll(readCloser) + if err != nil { + return nil, err + } + + readCloser.Close() + + return bytes.NewReader(data), nil +} diff --git a/internal/document/clean/contentType.go b/internal/document/clean/contentType.go new file mode 100644 index 00000000..a654568f --- /dev/null +++ b/internal/document/clean/contentType.go @@ -0,0 +1,110 @@ +package documentclean + +import ( + "bytes" + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +type MimeType string + +const ( + MimeTypePDF MimeType = "application/pdf" + MimeTypeBinaryOctetStream MimeType = "binary/octet-stream" + MimeTypeInvalid MimeType = "invalid" +) + +var AcceptMimeTypes = []MimeType{ + MimeTypePDF, +} + +const ( + PDFSignatureStr = "%PDF-" +) + +const ( + PDFSignatureEndStr = "%%EOF" +) + +var ( + PDFSignature = []byte(PDFSignatureStr) +) + +var ( + PDFSignatureEnd = []byte(PDFSignatureEndStr) +) + +func (s *Service) getAcceptedMimeType(ctx context.Context, params *CleanParams, contentType *string, length int64) (MimeType, error) { + mimeType := s.getMetadataMimeType(contentType) + if mimeType != MimeTypeInvalid && mimeType != MimeTypeBinaryOctetStream { + return mimeType, nil + } + + return s.getBodyMimeType(ctx, params, length) +} + +func (s *Service) getMetadataMimeType(contentType *string) MimeType { + if contentType == nil { + return MimeTypeInvalid + } + + parsedType := MimeType(*contentType) + + for _, accepted := range AcceptMimeTypes { + if accepted == parsedType { + return accepted + } + } + + return MimeTypeInvalid +} + +func (s *Service) getBodyMimeType(ctx context.Context, params *CleanParams, length int64) (MimeType, error) { + startBuffer, err := s.getBytesBuffer(ctx, params, 0, 5) + if err != nil { + return MimeTypeInvalid, err + } + endBuffer, err := s.getBytesBuffer(ctx, params, length-6, length) + if err != nil { + return MimeTypeInvalid, err + } + + prefixMimeType := MimeTypeInvalid + + if bytes.HasPrefix(startBuffer, PDFSignature) && bytes.HasSuffix(endBuffer, PDFSignatureEnd) { + prefixMimeType = MimeTypePDF + } + + return prefixMimeType, nil +} + +func (s *Service) getBytesBuffer(ctx context.Context, params *CleanParams, start int64, length int64) ([]byte, error) { + if start < 0 { + start = 0 + } + if length < 1 { + length = 1 + } + + byteRange := fmt.Sprintf("bytes=%d-%d", start, length-1) + out, err := s.cfg.GetStoreClient().GetObject(ctx, &s3.GetObjectInput{ + Bucket: ¶ms.Location.Bucket, + Key: ¶ms.Location.Key, + IfMatch: ¶ms.Hash, + Range: &byteRange, + }) + if err != nil { + return nil, err + } + defer out.Body.Close() + + buffer := make([]byte, length) + n, err := out.Body.Read(buffer) + if err != nil && n == 0 { + return nil, fmt.Errorf("unable to read object body: %v", err) + } + + return buffer[:n], nil +} diff --git a/internal/document/clean/contentType_test.go b/internal/document/clean/contentType_test.go new file mode 100644 index 00000000..d9d90d32 --- /dev/null +++ b/internal/document/clean/contentType_test.go @@ -0,0 +1,459 @@ +package documentclean + +import ( + "context" + "fmt" + "io" + "queryorchestration/internal/document" + objectstoremock "queryorchestration/mocks/objectstore" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetBodyMimeType(t *testing.T) { + t.Run("invalid type", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := "invalid" + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + contentType, err := svc.getBodyMimeType(ctx, params, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("pdf", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := fmt.Sprintf("%s %s", PDFSignatureStr, PDFSignatureEndStr) + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + contentType, err := svc.getBodyMimeType(ctx, params, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypePDF, contentType) + }) + t.Run("pdf start", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := fmt.Sprintf("%s---------------", PDFSignatureStr) + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + contentType, err := svc.getBodyMimeType(ctx, params, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("pdf end", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := fmt.Sprintf("%s---------------", PDFSignatureEndStr) + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + contentType, err := svc.getBodyMimeType(ctx, params, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("short", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := "" + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + _, err := svc.getBodyMimeType(ctx, params, int64(len(bodyStr))) + assert.Error(t, err) + }) +} + +func TestGetMetadataMimeType(t *testing.T) { + t.Run("nil type", func(t *testing.T) { + svc := Service{} + + contentType := svc.getMetadataMimeType(nil) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("invalid type", func(t *testing.T) { + svc := Service{} + + inType := "invalid_type" + contentType := svc.getMetadataMimeType(&inType) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("pdf", func(t *testing.T) { + svc := Service{} + + inType := "application/pdf" + contentType := svc.getMetadataMimeType(&inType) + assert.Equal(t, MimeTypePDF, contentType) + }) +} + +func TestGetAcceptedMimeType(t *testing.T) { + t.Run("invalid type", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := "invalid" + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + inType := "invalid_type" + contentType, err := svc.getAcceptedMimeType(ctx, params, &inType, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypeInvalid, contentType) + }) + t.Run("pdf mimetype", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + inType := "application/pdf" + contentType, err := svc.getAcceptedMimeType(ctx, &CleanParams{}, &inType, 0) + assert.NoError(t, err) + assert.Equal(t, MimeTypePDF, contentType) + }) + t.Run("pdf format", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + + bodyStr := fmt.Sprintf("%s %s", PDFSignatureStr, PDFSignatureEndStr) + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + inType := "invalid_type" + contentType, err := svc.getAcceptedMimeType(ctx, params, &inType, int64(len(bodyStr))) + assert.NoError(t, err) + assert.Equal(t, MimeTypePDF, contentType) + }) +} + +func TestGetBytesBuffer(t *testing.T) { + t.Run("valid", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + start := int64(0) + bodyStr := "invalid" + length := int64(len(bodyStr)) + + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash && *in.Range == "bytes=0-6" + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + buffer, err := svc.getBytesBuffer(ctx, params, start, length) + assert.NoError(t, err) + assert.NotNil(t, buffer) + }) + t.Run("negative start", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + start := int64(-1) + bodyStr := "invalid" + length := int64(len(bodyStr)) + + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash && *in.Range == "bytes=0-6" + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + buffer, err := svc.getBytesBuffer(ctx, params, start, length) + assert.NoError(t, err) + assert.NotNil(t, buffer) + }) + t.Run("zero length", func(t *testing.T) { + ctx := context.Background() + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + svc := Service{ + cfg: cfg, + } + + params := &CleanParams{ + Hash: "example_hash", + Location: document.Location{ + Bucket: "buck", + Key: "Key", + }, + } + start := int64(0) + bodyStr := "" + length := int64(0) + + body := io.NopCloser(strings.NewReader(bodyStr)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == params.Location.Bucket && *in.Key == params.Location.Key && *in.IfMatch == params.Hash && *in.Range == "bytes=0-0" + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + buffer, err := svc.getBytesBuffer(ctx, params, start, length) + assert.Error(t, err) + assert.Nil(t, buffer) + }) +} diff --git a/internal/document/clean/content_test.go b/internal/document/clean/content_test.go new file mode 100644 index 00000000..dcb573a4 --- /dev/null +++ b/internal/document/clean/content_test.go @@ -0,0 +1,59 @@ +package documentclean + +import ( + "context" + "io" + "queryorchestration/internal/document" + cleanversion "queryorchestration/internal/document/clean/version" + objectstoremock "queryorchestration/mocks/objectstore" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestGetContent(t *testing.T) { + t.Run("Working", func(t *testing.T) { + ctx := context.Background() + + cfg := &DocCleanConfig{} + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 + + svc := Service{ + cfg: cfg, + svc: &Services{ + Version: cleanversion.New(cfg), + }, + } + + inloc := document.Location{ + Bucket: "bucket_name", + Key: "/i/am/here", + } + params := &CleanParams{ + Hash: "hash", + Location: inloc, + } + mimeType := MimeTypePDF + + body := io.NopCloser(strings.NewReader(pdfHelloWorld)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + content, err := svc.getContent(ctx, params, mimeType) + assert.NoError(t, err) + assert.NotNil(t, content) + }) +} diff --git a/internal/document/clean/create.go b/internal/document/clean/create.go index 1fb6ab1b..c97ba3a4 100644 --- a/internal/document/clean/create.go +++ b/internal/document/clean/create.go @@ -10,7 +10,7 @@ import ( ) func (s *Service) Clean(ctx context.Context, id uuid.UUID) error { - isclean, err := s.cfg.GetDBQueries().IsDocumentClean(ctx, database.MustToDBUUID(id)) + isclean, err := s.cfg.GetDBQueries().HasDocumentCleanEntry(ctx, database.MustToDBUUID(id)) if err != nil { return err } diff --git a/internal/document/clean/create_test.go b/internal/document/clean/create_test.go index 040cb371..837b16f0 100644 --- a/internal/document/clean/create_test.go +++ b/internal/document/clean/create_test.go @@ -3,15 +3,20 @@ package documentclean import ( "context" "fmt" + "io" "queryorchestration/internal/database" "queryorchestration/internal/database/repository" "queryorchestration/internal/document" cleanversion "queryorchestration/internal/document/clean/version" "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/serviceconfig/objectstore" "queryorchestration/internal/serviceconfig/queue/documenttext" + objectstoremock "queryorchestration/mocks/objectstore" queuemock "queryorchestration/mocks/queue" + "strings" "testing" + "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/google/uuid" "github.com/pashagolub/pgxmock/v3" @@ -22,6 +27,7 @@ import ( type DocCleanConfig struct { serviceconfig.BaseConfig documenttext.DocTextConfig + objectstore.ObjectStoreConfig } func TestCreate(t *testing.T) { @@ -38,6 +44,8 @@ func TestCreate(t *testing.T) { cfg.DocumentTextURL = "/i/am/here" cfg.DBPool = pool cfg.DBQueries = repository.New(pool) + mockS3 := objectstoremock.NewMockS3Client(t) + cfg.StoreClient = mockS3 svc := Service{ cfg: cfg, @@ -46,34 +54,74 @@ func TestCreate(t *testing.T) { }, } - id := uuid.New() + doc := document.Document{ + ID: uuid.New(), + JobID: uuid.New(), + Hash: "example_hash", + } inloc := document.Location{ Bucket: "bucket_name", Key: "/i/am/here", } + dbid := database.MustToDBUUID(doc.ID) - pool.ExpectQuery("name: IsDocumentClean :one").WithArgs(database.MustToDBUUID(id)).WillReturnRows( + pool.ExpectQuery("name: HasDocumentCleanEntry :one").WithArgs(dbid).WillReturnRows( pgxmock.NewRows([]string{"isclean"}). AddRow(false), ) - pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(database.MustToDBUUID(id)).WillReturnRows( + pool.ExpectQuery("name: GetDocument :one").WithArgs(dbid). + WillReturnRows( + pgxmock.NewRows([]string{"id", "jobId", "hash"}). + AddRow(dbid, database.MustToDBUUID(doc.JobID), doc.Hash), + ) + pool.ExpectQuery("name: GetDocumentEntry :one").WithArgs(dbid).WillReturnRows( pgxmock.NewRows([]string{"documentId", "bucket", "key"}). - AddRow(database.MustToDBUUID(id), inloc.Bucket, inloc.Key), + AddRow(dbid, inloc.Bucket, inloc.Key), ) - pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(database.MustToDBUUID(id), int32(1), inloc.Bucket, inloc.Key). + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } + pool.ExpectExec("name: AddDocumentCleanEntry :exec").WithArgs(dbid, int32(1), &inloc.Bucket, &inloc.Key, dbmimetype, repository.NullCleanfailtype{}). WillReturnResult(pgxmock.NewResult("", 1)) mockSQS.EXPECT(). SendMessage( mock.Anything, mock.MatchedBy(func(in *sqs.SendMessageInput) bool { - return *in.QueueUrl == cfg.DocumentTextURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", id) + return *in.QueueUrl == cfg.DocumentTextURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", doc.ID) }), mock.Anything, ). Return(&sqs.SendMessageOutput{}, nil) - err = svc.Clean(ctx, id) + mockS3.EXPECT(). + HeadObject( + mock.Anything, + mock.MatchedBy(func(in *s3.HeadObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.HeadObjectOutput{ + ContentType: &mimeType, + }, nil) + + body := io.NopCloser(strings.NewReader(pdfHelloWorld)) + mockS3.EXPECT(). + GetObject( + mock.Anything, + mock.MatchedBy(func(in *s3.GetObjectInput) bool { + return *in.Bucket == inloc.Bucket && *in.Key == inloc.Key + }), + mock.Anything, + ). + Return(&s3.GetObjectOutput{ + Body: body, + }, nil) + + err = svc.Clean(ctx, doc.ID) assert.NoError(t, err) } diff --git a/internal/document/clean/pdf.go b/internal/document/clean/pdf.go new file mode 100644 index 00000000..8ead5308 --- /dev/null +++ b/internal/document/clean/pdf.go @@ -0,0 +1,54 @@ +package documentclean + +import ( + "context" + "io" + + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" +) + +type PDF struct { + pdf io.ReadSeeker + config *model.Configuration + maxPages int + maxBytes int64 +} + +const ( + TEXTRACT_SYNC_LIMIT = 10 * 1024 * 1024 + TEXTRACT_ASYNC_LIMIT = 500 * 1024 * 1024 + TEXTRACT_PAGE_LIMIT = 3000 +) + +func NewPDF(buf io.ReadSeeker) *PDF { + config := &model.Configuration{ + ValidationMode: model.ValidationRelaxed, + Reader15: true, + Offline: true, + } + + return &PDF{ + pdf: buf, + config: config, + maxPages: TEXTRACT_PAGE_LIMIT, + maxBytes: TEXTRACT_SYNC_LIMIT, + } +} + +func (s *PDF) isCorrupted(ctx context.Context) *InvalidDocumentReason { + var reason InvalidDocumentReason + + pdfCtx, err := pdfcpu.ReadWithContext(ctx, s.pdf, s.config) + if err != nil { + reason = InvalidDocumentRead + return &reason + } + + if int64(pdfCtx.Read.ReadFileSize()) > s.maxBytes { + reason = InvalidDocumentLargeFile + return &reason + } + + return nil +} diff --git a/internal/document/clean/pdf_test.go b/internal/document/clean/pdf_test.go new file mode 100644 index 00000000..61459b19 --- /dev/null +++ b/internal/document/clean/pdf_test.go @@ -0,0 +1,43 @@ +package documentclean + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsCorrupt(t *testing.T) { + t.Run("valid", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfHelloWorld)) + + reason := pdf.isCorrupted(ctx) + assert.Nil(t, reason) + }) + t.Run("no content", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader("%PDF-1.4 %%EOF")) + + reason := pdf.isCorrupted(ctx) + assert.Equal(t, InvalidDocumentRead, *reason) + }) + t.Run("large file size", func(t *testing.T) { + ctx := context.Background() + pdf := NewPDF(strings.NewReader(pdfHelloWorld)) + pdf.maxBytes = 1 + + reason := pdf.isCorrupted(ctx) + assert.Equal(t, InvalidDocumentLargeFile, *reason) + }) +} + +func TestNewPDF(t *testing.T) { + t.Run("valid", func(t *testing.T) { + pdf := NewPDF(strings.NewReader(pdfHelloWorld)) + assert.NotNil(t, pdf.pdf) + assert.NotNil(t, pdf.config) + assert.Equal(t, 3000, pdf.maxPages) + }) +} diff --git a/internal/document/clean/service.go b/internal/document/clean/service.go index 784a655b..cb55569b 100644 --- a/internal/document/clean/service.go +++ b/internal/document/clean/service.go @@ -3,12 +3,14 @@ package documentclean import ( cleanversion "queryorchestration/internal/document/clean/version" "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/serviceconfig/objectstore" "queryorchestration/internal/serviceconfig/queue/documenttext" ) type ConfigProvider interface { serviceconfig.ConfigProvider documenttext.ConfigProvider + objectstore.ConfigProvider } type Services struct { diff --git a/internal/document/clean/service_test.go b/internal/document/clean/service_test.go index 17a965c4..d931cf56 100644 --- a/internal/document/clean/service_test.go +++ b/internal/document/clean/service_test.go @@ -3,6 +3,7 @@ package documentclean_test import ( documentclean "queryorchestration/internal/document/clean" "queryorchestration/internal/serviceconfig" + "queryorchestration/internal/serviceconfig/objectstore" "queryorchestration/internal/serviceconfig/queue/documenttext" "testing" @@ -12,6 +13,7 @@ import ( type DocCleanConfig struct { serviceconfig.BaseConfig documenttext.DocTextConfig + objectstore.ConfigProvider } func TestService(t *testing.T) { diff --git a/internal/document/clean/version/version.go b/internal/document/clean/version/version.go index defa953e..f27489fc 100644 --- a/internal/document/clean/version/version.go +++ b/internal/document/clean/version/version.go @@ -5,7 +5,6 @@ import ( ) func (s *Service) GetVersion() int32 { - // TODO - actual version return 1 } diff --git a/internal/document/text/create_test.go b/internal/document/text/create_test.go index dd0e8265..8b71267f 100644 --- a/internal/document/text/create_test.go +++ b/internal/document/text/create_test.go @@ -57,9 +57,14 @@ func TestCreate(t *testing.T) { AddRow(false), ) cleanId := database.MustToDBUUID(uuid.New()) + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } pool.ExpectQuery("name: GetDocumentCleanEntry :one").WithArgs(database.MustToDBUUID(id)).WillReturnRows( - pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version"}). - AddRow(cleanId, database.MustToDBUUID(id), inloc.Bucket, inloc.Key, int32(1)), + pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "mimetype", "fail"}). + AddRow(cleanId, database.MustToDBUUID(id), &inloc.Bucket, &inloc.Key, int32(1), dbmimetype, repository.NullCleanfailtype{}), ) pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(int32(1), inloc.Bucket, inloc.Key, cleanId). WillReturnResult(pgxmock.NewResult("", 1)) diff --git a/internal/document/text/extract.go b/internal/document/text/extract.go index 94bb6259..46471cce 100644 --- a/internal/document/text/extract.go +++ b/internal/document/text/extract.go @@ -2,6 +2,7 @@ package documenttext import ( "context" + "fmt" "queryorchestration/internal/database" "queryorchestration/internal/database/repository" "queryorchestration/internal/document" @@ -15,7 +16,6 @@ type ExtractionParams struct { } func (s *Service) executeExtraction(params *ExtractionParams) (*document.Location, error) { - // TODO - various extraction tasks return ¶ms.Location, nil } @@ -25,13 +25,15 @@ func (s *Service) extract(ctx context.Context, id uuid.UUID) error { entry, err := s.cfg.GetDBQueries().GetDocumentCleanEntry(ctx, docId) if err != nil { return err + } else if entry.Fail.Valid { + return fmt.Errorf("no valid cleaning") } outLocation, err := s.executeExtraction(&ExtractionParams{ ID: id, Location: document.Location{ - Bucket: entry.Bucket, - Key: entry.Key, + Bucket: *entry.Bucket, + Key: *entry.Key, }, }) if err != nil { diff --git a/internal/document/text/extract_test.go b/internal/document/text/extract_test.go index 73c10ca3..c5eec9f7 100644 --- a/internal/document/text/extract_test.go +++ b/internal/document/text/extract_test.go @@ -38,9 +38,14 @@ func TestExtract(t *testing.T) { } cleanId := database.MustToDBUUID(uuid.New()) + mimeType := "application/pdf" + dbmimetype := repository.NullCleanmimetypes{ + Valid: true, + Cleanmimetypes: repository.Cleanmimetypes(mimeType), + } pool.ExpectQuery("name: GetDocumentCleanEntry :one").WithArgs(database.MustToDBUUID(id)).WillReturnRows( - pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version"}). - AddRow(cleanId, database.MustToDBUUID(id), inloc.Bucket, inloc.Key, int32(1)), + pgxmock.NewRows([]string{"id", "documentId", "bucket", "key", "version", "mimetype", "fail"}). + AddRow(cleanId, database.MustToDBUUID(id), &inloc.Bucket, &inloc.Key, int32(1), dbmimetype, repository.NullCleanfailtype{}), ) pool.ExpectExec("name: AddDocumentTextEntry :exec").WithArgs(int32(1), inloc.Bucket, inloc.Key, cleanId). WillReturnResult(pgxmock.NewResult("", 1)) diff --git a/internal/document/text/version/version.go b/internal/document/text/version/version.go index 7e3ce64a..94ff117d 100644 --- a/internal/document/text/version/version.go +++ b/internal/document/text/version/version.go @@ -5,7 +5,6 @@ import ( ) func (s *Service) GetVersion() int32 { - // TODO - actual version return 1 } diff --git a/internal/query/types/contextFull/creator.go b/internal/query/types/contextFull/creator.go index 0bebe4da..d7492a5a 100644 --- a/internal/query/types/contextFull/creator.go +++ b/internal/query/types/contextFull/creator.go @@ -13,7 +13,5 @@ func NewCreator() *Creator { } func (s *Creator) Validate(ctx context.Context, entity *resultprocessor.Create) error { - // TODO - // Type, RequiredQueryIDs, Config return nil } diff --git a/internal/query/types/contextFull/result.go b/internal/query/types/contextFull/result.go index 97809efc..8ac7f279 100644 --- a/internal/query/types/contextFull/result.go +++ b/internal/query/types/contextFull/result.go @@ -13,7 +13,6 @@ func NewResult(value string) *Result { } func (r *Result) GetValue(ctx context.Context) (string, error) { - // TODO - get value from s3 return r.value, nil } diff --git a/internal/query/types/contextFull/service.go b/internal/query/types/contextFull/service.go index 16ef84f2..be7c7a15 100644 --- a/internal/query/types/contextFull/service.go +++ b/internal/query/types/contextFull/service.go @@ -17,7 +17,6 @@ func (e *Extractor) Process(ctx context.Context, query *resultprocessor.Query, v if len(values) > 0 { return "", errors.New("no requirements expected") } - // TODO return `{"keyone":"valueone","keytwo":"valuetwo"}`, nil } diff --git a/internal/query/types/contextFull/updator.go b/internal/query/types/contextFull/updator.go index 935652eb..7b8342eb 100644 --- a/internal/query/types/contextFull/updator.go +++ b/internal/query/types/contextFull/updator.go @@ -12,7 +12,5 @@ func NewUpdator() *Updator { } func (s *Updator) Validate(ctx context.Context, current *resultprocessor.Query, entity *resultprocessor.Update) error { - // TODO - // Type, RequiredQueryIDs, Config return nil } diff --git a/internal/query/types/jsonExtractor/creator.go b/internal/query/types/jsonExtractor/creator.go index 2e3c1f5f..0fa53e93 100644 --- a/internal/query/types/jsonExtractor/creator.go +++ b/internal/query/types/jsonExtractor/creator.go @@ -16,6 +16,6 @@ func (s *Creator) Validate(ctx context.Context, entity *resultprocessor.Create) if entity.Config == nil { return errors.New("config required") } - // TODO + return nil } diff --git a/internal/query/types/jsonExtractor/updator.go b/internal/query/types/jsonExtractor/updator.go index bd2a307d..28916bb5 100644 --- a/internal/query/types/jsonExtractor/updator.go +++ b/internal/query/types/jsonExtractor/updator.go @@ -13,6 +13,5 @@ func NewUpdator() *Updator { } func (s *Updator) Validate(ctx context.Context, current *resultprocessor.Query, entity *resultprocessor.Update) error { - // TODO return nil } diff --git a/internal/serviceconfig/queue/delete.go b/internal/serviceconfig/queue/delete.go index 423baad0..88a68fe3 100644 --- a/internal/serviceconfig/queue/delete.go +++ b/internal/serviceconfig/queue/delete.go @@ -14,7 +14,7 @@ type DeleteParams struct { } func (c *QueueConfig) DeleteFromQueue(ctx context.Context, params *DeleteParams) error { - slog.Debug("deleting message from queue", "url", params.QueueURL, "receipt_handle", params.ReceiptHandle) + slog.Debug("deleting message from queue", "url", params.QueueURL, "receipt_handle", *params.ReceiptHandle) _, err := c.QueueClient.DeleteMessage(ctx, &sqs.DeleteMessageInput{ QueueUrl: aws.String(params.QueueURL), diff --git a/scripts/local-deployments.yml b/scripts/local-deployments.yml index de85b60f..46fd546c 100644 --- a/scripts/local-deployments.yml +++ b/scripts/local-deployments.yml @@ -1,7 +1,7 @@ --- # https://taskfile.dev -version: '3' +version: "3" vars: LOCAL_COMPOSE_FILE: "deployments/compose.local.yaml" @@ -13,33 +13,33 @@ tasks: cmds: - task: build vars: - COMPOSE_FILE: '{{.TEST_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" build:generate: cmds: - task: build vars: - COMPOSE_FILE: '{{.GENERATE_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" build: vars: - COMPOSE_FILE: '{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" cmds: - docker compose -f {{.COMPOSE_FILE}} build up:cmd: internal: true vars: - COMPOSE_FILE: '{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" cmds: - docker compose -f {{.COMPOSE_FILE}} up --no-recreate --wait -d up:test: cmds: - task: up:cmd vars: - COMPOSE_FILE: '{{.TEST_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" up:generate: cmds: - task: up:cmd vars: - COMPOSE_FILE: '{{.GENERATE_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" up: cmds: - task docker:build @@ -77,15 +77,15 @@ tasks: cmds: - task: down vars: - COMPOSE_FILE: '{{.TEST_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.TEST_COMPOSE_FILE}}" down:generate: cmds: - task: down vars: - COMPOSE_FILE: '{{.GENERATE_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.GENERATE_COMPOSE_FILE}}" down: vars: - COMPOSE_FILE: '{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}' + COMPOSE_FILE: "{{.COMPOSE_FILE | default .LOCAL_COMPOSE_FILE}}" cmds: - docker compose -f {{.COMPOSE_FILE}} down clean:test: diff --git a/test/process_test.go b/test/process_test.go index 8796e94a..b8fc6dcd 100644 --- a/test/process_test.go +++ b/test/process_test.go @@ -197,7 +197,7 @@ func TestProcess(t *testing.T) { assert.Equal(t, queryservice.INSYNC, jRes.JSON200.Status) location := fmt.Sprintf("%s/%s/%s", clientRes.JSON201.Id, jobRes.JSON201.Id, "object_name") - body := strings.NewReader(`{"keyone":"valueone","keytwo":"valuetwo"}`) + body := strings.NewReader(pdfHelloWorld) _, err = cfg.StoreClient.PutObject(ctx, &s3.PutObjectInput{ Bucket: &bucketName, Key: &location, @@ -243,6 +243,8 @@ func TestProcess(t *testing.T) { } func WaitForJobStatus(t testing.TB, ctx context.Context, service *queryservice.ClientWithResponses, id types.UUID, status queryservice.JobStatus) { + t.Helper() + timeout := time.After(30 * time.Second) ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() @@ -264,3 +266,65 @@ func WaitForJobStatus(t testing.TB, ctx context.Context, service *queryservice.C } } } + +const pdfHelloWorld = `%PDF-1.4 +%���� +1 0 obj +<< + /Type /Catalog + /Pages 2 0 R + /Version /1.4 +>> +endobj +2 0 obj +<< + /Type /Pages + /Kids [3 0 R] + /Count 1 +>> +endobj +3 0 obj +<< + /Type /Page + /Parent 2 0 R + /MediaBox [0 0 612 792] + /Resources << + /Font << + /F1 << + /Type /Font + /Subtype /Type1 + /BaseFont /Helvetica + >> + >> + >> + /Contents 4 0 R +>> +endobj +4 0 obj +<< + /Length + 44 +>> +stream +BT +/F1 24 Tf +100 700 Td +(Hello World!) Tj +ET +endstream +endobj +xref +0 5 +0000000000 65535 f +0000000015 00000 n +0000000086 00000 n +0000000151 00000 n +0000000376 00000 n +trailer +<< + /Size 5 + /Root 1 0 R +>> +startxref +472 +%%EOF` diff --git a/vendor/github.com/hhrutter/lzw/.gitignore b/vendor/github.com/hhrutter/lzw/.gitignore new file mode 100644 index 00000000..31651d63 --- /dev/null +++ b/vendor/github.com/hhrutter/lzw/.gitignore @@ -0,0 +1,6 @@ +# Mac +**/.DS_Store +**/._.DS_Store + +# VSCode +.vscode/* \ No newline at end of file diff --git a/vendor/github.com/hhrutter/lzw/LICENSE b/vendor/github.com/hhrutter/lzw/LICENSE new file mode 100644 index 00000000..6a66aea5 --- /dev/null +++ b/vendor/github.com/hhrutter/lzw/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/hhrutter/lzw/README.md b/vendor/github.com/hhrutter/lzw/README.md new file mode 100644 index 00000000..bdee415e --- /dev/null +++ b/vendor/github.com/hhrutter/lzw/README.md @@ -0,0 +1,37 @@ +# Note + +* This is a consolidated version of [compress/lzw](https://github.com/golang/go/tree/master/src/compress/lzw) that supports GIF, TIFF and PDF. +* Please refer to this [golang proposal](https://github.com/golang/go/issues/25409) for details. +* [github.com/hhrutter/tiff](https://github.com/hhrutter/tiff) uses this package to extend [x/image/tiff](https://github.com/golang/image/tree/master/tiff). +* [pdfcpu](https://github.com/pdfcpu/pdfcpu) uses this package for processing PDFs with embedded TIFF images. + + +## Background + +* PDF's LZWDecode filter comes with the optional parameter `EarlyChange`. +* The type of this parameter is `int` and the defined values are 0 and 1. +* The default value is 1. + +This parameter implies two variants of lzw. (See the [PDF spec](https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf)). + +[compress/lzw](https://github.com/golang/go/tree/master/src/compress/lzw): + +* the algorithm implied by EarlyChange value 1 +* provides both Reader and Writer. + +[x/image/tiff/lzw](https://github.com/golang/image/tree/master/tiff/lzw): + +* the algorithm implied by EarlyChange value 0 +* provides a Reader, lacks a Writer + +In addition PDF expects a leading `clear_table` marker right at the beginning +which is not something [compress/lzw](https://github.com/golang/go/tree/master/src/compress/lzw) takes into account. + +There are numerous PDF Writers out there and for arbitrary PDF files using the LZWDecode filter the following can be observed: + +* Some PDF writers do not write the EOD (end of data) marker. +* Some PDF writers do not write the final bits after the EOD marker. + +## Goal + +An extended version of [compress/lzw](https://github.com/golang/go/tree/master/src/compress/lzw) with reliable support for GIF, TIFF and PDF. diff --git a/vendor/github.com/hhrutter/lzw/reader.go b/vendor/github.com/hhrutter/lzw/reader.go new file mode 100644 index 00000000..7230a1df --- /dev/null +++ b/vendor/github.com/hhrutter/lzw/reader.go @@ -0,0 +1,238 @@ +// Copyright 2011 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. + +// Package lzw is an enhanced version of compress/lzw. +// +// It implements Adobe's PDF lzw compression as defined for the LZWDecode filter +// and is also compatible with the TIFF file format. +// +// See the golang proposal: https://github.com/golang/go/issues/25409. +// +// More information: https://github.com/pdfcpu/pdfcpu/tree/master/lzw +package lzw + +import ( + "bufio" + "errors" + "io" +) + +const ( + maxWidth = 12 + decoderInvalidCode = 0xffff + flushBuffer = 1 << maxWidth +) + +// decoder is the state from which the readXxx method converts a byte +// stream into a code stream. +type decoder struct { + r io.ByteReader + bits uint32 + nBits uint + width uint + read func(*decoder) (uint16, error) // readMSB always for PDF and TIFF + litWidth uint // width in bits of literal codes + err error + + // The first 1<= 1<> (32 - d.width)) + d.bits <<= d.width + d.nBits -= d.width + return code, nil +} + +func (d *decoder) Read(b []byte) (int, error) { + for { + if len(d.toRead) > 0 { + n := copy(b, d.toRead) + d.toRead = d.toRead[n:] + return n, nil + } + if d.err != nil { + return 0, d.err + } + d.decode() + } +} + +func (d *decoder) handleOverflow() { + ui := d.hi + if d.oneOff { + ui++ + } + if ui >= d.overflow { + if d.width == maxWidth { + d.last = decoderInvalidCode + // Undo the d.hi++ a few lines above, so that (1) we maintain + // the invariant that d.hi <= d.overflow, and (2) d.hi does not + // eventually overflow a uint16. + if !d.oneOff { + d.hi-- + } + } else { + d.width++ + d.overflow <<= 1 + } + } +} + +// decode decompresses bytes from r and leaves them in d.toRead. +// read specifies how to decode bytes into codes. +// litWidth is the width in bits of literal codes. +func (d *decoder) decode() { + i := 0 + // Loop over the code stream, converting codes into decompressed bytes. +loop: + for { + code, err := d.read(d) + i++ + if err != nil { + // Some PDF Writers write an EOD some don't. + // Don't insist on EOD marker. + // Don't return an unexpected EOF error. + d.err = err + break + } + switch { + case code < d.clear: + // We have a literal code. + d.output[d.o] = uint8(code) + d.o++ + if d.last != decoderInvalidCode { + // Save what the hi code expands to. + d.suffix[d.hi] = uint8(code) + d.prefix[d.hi] = d.last + } + case code == d.clear: + d.width = 1 + d.litWidth + d.hi = d.eof + d.overflow = 1 << d.width + d.last = decoderInvalidCode + continue + case code == d.eof: + d.err = io.EOF + break loop + case code <= d.hi: + c, i := code, len(d.output)-1 + if code == d.hi && d.last != decoderInvalidCode { + // code == hi is a special case which expands to the last expansion + // followed by the head of the last expansion. To find the head, we walk + // the prefix chain until we find a literal code. + c = d.last + for c >= d.clear { + c = d.prefix[c] + } + d.output[i] = uint8(c) + i-- + c = d.last + } + // Copy the suffix chain into output and then write that to w. + for c >= d.clear { + d.output[i] = d.suffix[c] + i-- + c = d.prefix[c] + } + d.output[i] = uint8(c) + d.o += copy(d.output[d.o:], d.output[i:]) + if d.last != decoderInvalidCode { + // Save what the hi code expands to. + d.suffix[d.hi] = uint8(c) + d.prefix[d.hi] = d.last + } + default: + d.err = errors.New("lzw: invalid code") + break loop + } + d.last, d.hi = code, d.hi+1 + d.handleOverflow() + if d.o >= flushBuffer { + break + } + } + // Flush pending output. + d.toRead = d.output[:d.o] + d.o = 0 +} + +var errClosed = errors.New("lzw: reader/writer is closed") + +func (d *decoder) Close() error { + d.err = errClosed // in case any Reads come along + return nil +} + +// NewReader creates a new io.ReadCloser. +// Reads from the returned io.ReadCloser read and decompress data from r. +// If r does not also implement io.ByteReader, +// the decompressor may read more data than necessary from r. +// It is the caller's responsibility to call Close on the ReadCloser when +// finished reading. +// oneOff makes code length increases occur one code early. It should be true +// for LZWDecode filters with earlyChange=1 which is also the default. +func NewReader(r io.Reader, oneOff bool) io.ReadCloser { + + br, ok := r.(io.ByteReader) + if !ok { + br = bufio.NewReader(r) + } + + lw := uint(8) + clear := uint16(1) << lw + width := 1 + lw + + return &decoder{ + r: br, + read: (*decoder).readMSB, + litWidth: lw, + width: width, + clear: clear, + eof: clear + 1, + hi: clear + 1, + overflow: uint16(1) << width, + last: decoderInvalidCode, + oneOff: oneOff, + } +} diff --git a/vendor/github.com/hhrutter/lzw/writer.go b/vendor/github.com/hhrutter/lzw/writer.go new file mode 100644 index 00000000..dd42b12c --- /dev/null +++ b/vendor/github.com/hhrutter/lzw/writer.go @@ -0,0 +1,283 @@ +// Derived from compress/lzw in order to implement +// Adobe's PDF lzw compression as defined for the LZWDecode filter. +// See https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf +// and https://github.com/golang/go/issues/25409. +// +// It is also compatible with the TIFF file format. +// +// Copyright 2011 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. + +package lzw + +import ( + "bufio" + "errors" + "io" +) + +// A writer is a buffered, flushable writer. +type writer interface { + io.ByteWriter + Flush() error +} + +// An errWriteCloser is an io.WriteCloser that always returns a given error. +type errWriteCloser struct { + err error +} + +func (e *errWriteCloser) Write([]byte) (int, error) { + return 0, e.err +} + +func (e *errWriteCloser) Close() error { + return e.err +} + +const ( + // A code is a 12 bit value, stored as a uint32 when encoding to avoid + // type conversions when shifting bits. + maxCode = 1<<12 - 1 + invalidCode = 1<<32 - 1 + // There are 1<<12 possible codes, which is an upper bound on the number of + // valid hash table entries at any given point in time. tableSize is 4x that. + tableSize = 4 * 1 << 12 + tableMask = tableSize - 1 + // A hash table entry is a uint32. Zero is an invalid entry since the + // lower 12 bits of a valid entry must be a non-literal code. + invalidEntry = 0 +) + +// encoder is LZW compressor. +type encoder struct { + // w is the writer that compressed bytes are written to. + w writer + // write, bits, nBits and width are the state for + // converting a code stream into a byte stream. + write func(*encoder, uint32) error + bits uint32 + nBits uint + width uint + // litWidth is the width in bits of literal codes. + litWidth uint + // hi is the code implied by the next code emission. + // overflow is the code at which hi overflows the code width. + hi, overflow uint32 + // savedCode is the accumulated code at the end of the most recent Write + // call. It is equal to invalidCode if there was no such call. + savedCode uint32 + // err is the first error encountered during writing. Closing the encoder + // will make any future Write calls return errClosed + err error + // table is the hash table from 20-bit keys to 12-bit values. Each table + // entry contains key<<12|val and collisions resolve by linear probing. + // The keys consist of a 12-bit code prefix and an 8-bit byte suffix. + // The values are a 12-bit code. + table [tableSize]uint32 + // oneOff makes code length increases occur one code early. + oneOff bool +} + +// writeLSB writes the code c for "Least Significant Bits first" data. +func (e *encoder) writeLSB(c uint32) error { + e.bits |= c << e.nBits + e.nBits += e.width + for e.nBits >= 8 { + if err := e.w.WriteByte(uint8(e.bits)); err != nil { + return err + } + e.bits >>= 8 + e.nBits -= 8 + } + return nil +} + +// writeMSB writes the code c for "Most Significant Bits first" data. +func (e *encoder) writeMSB(c uint32) error { + e.bits |= c << (32 - e.width - e.nBits) + e.nBits += e.width + for e.nBits >= 8 { + if err := e.w.WriteByte(uint8(e.bits >> 24)); err != nil { + return err + } + e.bits <<= 8 + e.nBits -= 8 + } + return nil +} + +// errOutOfCodes is an internal error that means that the encoder has run out +// of unused codes and a clear code needs to be sent next. +var errOutOfCodes = errors.New("lzw: out of codes") + +// incHi increments e.hi and checks for both overflow and running out of +// unused codes. In the latter case, incHi sends a clear code, resets the +// encoder state and returns errOutOfCodes. +func (e *encoder) incHi() error { + e.hi++ + + // The PDF spec defines for the LZWDecode filter a parameter "EarlyChange". + // This parameter drives the variation of lzw compression to be used. + // The standard compress/lzw does not know about oneOff. + ui := e.hi + if e.oneOff { + ui++ + } + + if ui == e.overflow { + e.width++ + e.overflow <<= 1 + } + + if ui == maxCode { + clear := uint32(1) << e.litWidth + if err := e.write(e, clear); err != nil { + return err + } + e.width = e.litWidth + 1 + e.hi = clear + 1 + e.overflow = clear << 1 + for i := range e.table { + e.table[i] = invalidEntry + } + return errOutOfCodes + } + return nil +} + +// Write writes a compressed representation of p to e's underlying writer. +func (e *encoder) Write(p []byte) (n int, err error) { + if e.err != nil { + return 0, e.err + } + if len(p) == 0 { + return 0, nil + } + if maxLit := uint8(1< maxLit { + e.err = errors.New("lzw: input byte too large for the litWidth") + return 0, e.err + } + } + } + + n = len(p) + code := e.savedCode + if code == invalidCode { + // The first code sent is always a literal code. + code, p = uint32(p[0]), p[1:] + } +loop: + for _, x := range p { + literal := uint32(x) + key := code<<8 | literal + // If there is a hash table hit for this key then we continue the loop + // and do not emit a code yet. + hash := (key>>12 ^ key) & tableMask + for h, t := hash, e.table[hash]; t != invalidEntry; { + if key == t>>12 { + code = t & maxCode + continue loop + } + h = (h + 1) & tableMask + t = e.table[h] + } + // Otherwise, write the current code, and literal becomes the start of + // the next emitted code. + if e.err = e.write(e, code); e.err != nil { + return 0, e.err + } + code = literal + // Increment e.hi, the next implied code. If we run out of codes, reset + // the encoder state (including clearing the hash table) and continue. + if err1 := e.incHi(); err1 != nil { + if err1 == errOutOfCodes { + continue + } + e.err = err1 + return 0, e.err + } + // Otherwise, insert key -> e.hi into the map that e.table represents. + for { + if e.table[hash] == invalidEntry { + e.table[hash] = (key << 12) | e.hi + break + } + hash = (hash + 1) & tableMask + } + } + e.savedCode = code + return n, nil +} + +// Close closes the encoder, flushing any pending output. It does not close or +// flush e's underlying writer. +func (e *encoder) Close() error { + if e.err != nil { + if e.err == errClosed { + return nil + } + return e.err + } + // Make any future calls to Write return errClosed. + e.err = errClosed + // Write the savedCode if valid. + if e.savedCode != invalidCode { + if err := e.write(e, e.savedCode); err != nil { + return err + } + if err := e.incHi(); err != nil && err != errOutOfCodes { + return err + } + } + // Write the eof code. + eof := uint32(1)< 0 { + e.bits >>= 24 + if err := e.w.WriteByte(uint8(e.bits)); err != nil { + return err + } + } + return e.w.Flush() +} + +// NewWriter creates a new io.WriteCloser. +// Writes to the returned io.WriteCloser are compressed and written to w. +// It is the caller's responsibility to call Close on the WriteCloser when +// finished writing. +// oneOff makes code length increases occur one code early. It should be true +// for LZWDecode filters with earlyChange=1 which is also the default. +func NewWriter(w io.Writer, oneOff bool) io.WriteCloser { + + bw, ok := w.(writer) + if !ok { + bw = bufio.NewWriter(w) + } + + lw := uint(8) + + e := encoder{ + w: bw, + write: (*encoder).writeMSB, + litWidth: lw, + width: 1 + lw, + hi: 1< m { + if end > cap(b.buf) { + newcap := 1024 + for newcap < end { + newcap *= 2 + } + newbuf := make([]byte, end, newcap) + copy(newbuf, b.buf) + b.buf = newbuf + } else { + b.buf = b.buf[:end] + } + if n, err := io.ReadFull(b.r, b.buf[m:end]); err != nil { + end = m + n + b.buf = b.buf[:end] + return err + } + } + return nil +} + +func (b *buffer) ReadAt(p []byte, off int64) (int, error) { + o := int(off) + end := o + len(p) + if int64(end) != off+int64(len(p)) { + return 0, io.ErrUnexpectedEOF + } + + err := b.fill(end) + return copy(p, b.buf[o:end]), err +} + +// Slice returns a slice of the underlying buffer. The slice contains +// n bytes starting at offset off. +func (b *buffer) Slice(off, n int) ([]byte, error) { + end := off + n + if err := b.fill(end); err != nil { + return nil, err + } + return b.buf[off:end], nil +} + +// newReaderAt converts an io.Reader into an io.ReaderAt. +func newReaderAt(r io.Reader) io.ReaderAt { + if ra, ok := r.(io.ReaderAt); ok { + return ra + } + return &buffer{ + r: r, + buf: make([]byte, 0, 1024), + } +} diff --git a/vendor/github.com/hhrutter/tiff/compress.go b/vendor/github.com/hhrutter/tiff/compress.go new file mode 100644 index 00000000..3f176f00 --- /dev/null +++ b/vendor/github.com/hhrutter/tiff/compress.go @@ -0,0 +1,58 @@ +// Copyright 2011 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. + +package tiff + +import ( + "bufio" + "io" +) + +type byteReader interface { + io.Reader + io.ByteReader +} + +// unpackBits decodes the PackBits-compressed data in src and returns the +// uncompressed data. +// +// The PackBits compression format is described in section 9 (p. 42) +// of the TIFF spec. +func unpackBits(r io.Reader) ([]byte, error) { + buf := make([]byte, 128) + dst := make([]byte, 0, 1024) + br, ok := r.(byteReader) + if !ok { + br = bufio.NewReader(r) + } + + for { + b, err := br.ReadByte() + if err != nil { + if err == io.EOF { + return dst, nil + } + return nil, err + } + code := int(int8(b)) + switch { + case code >= 0: + n, err := io.ReadFull(br, buf[:code+1]) + if err != nil { + return nil, err + } + dst = append(dst, buf[:n]...) + case code == -128: + // No-op. + default: + if b, err = br.ReadByte(); err != nil { + return nil, err + } + for j := 0; j < 1-code; j++ { + buf[j] = b + } + dst = append(dst, buf[:1-code]...) + } + } +} diff --git a/vendor/github.com/hhrutter/tiff/consts.go b/vendor/github.com/hhrutter/tiff/consts.go new file mode 100644 index 00000000..3e5f7f14 --- /dev/null +++ b/vendor/github.com/hhrutter/tiff/consts.go @@ -0,0 +1,149 @@ +// Copyright 2011 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. + +package tiff + +// A tiff image file contains one or more images. The metadata +// of each image is contained in an Image File Directory (IFD), +// which contains entries of 12 bytes each and is described +// on page 14-16 of the specification. An IFD entry consists of +// +// - a tag, which describes the signification of the entry, +// - the data type and length of the entry, +// - the data itself or a pointer to it if it is more than 4 bytes. +// +// The presence of a length means that each IFD is effectively an array. + +const ( + leHeader = "II\x2A\x00" // Header for little-endian files. + beHeader = "MM\x00\x2A" // Header for big-endian files. + + ifdLen = 12 // Length of an IFD entry in bytes. +) + +// Data types (p. 14-16 of the spec). +const ( + dtByte = 1 + dtASCII = 2 + dtShort = 3 + dtLong = 4 + dtRational = 5 +) + +// The length of one instance of each data type in bytes. +var lengths = [...]uint32{0, 1, 1, 2, 4, 8} + +// Tags (see p. 28-41 of the spec). +const ( + tImageWidth = 256 + tImageLength = 257 + tBitsPerSample = 258 + tCompression = 259 + tPhotometricInterpretation = 262 + + tFillOrder = 266 + + tStripOffsets = 273 + tSamplesPerPixel = 277 + tRowsPerStrip = 278 + tStripByteCounts = 279 + + tT4Options = 292 // CCITT Group 3 options, a set of 32 flag bits. + tT6Options = 293 // CCITT Group 4 options, a set of 32 flag bits. + + tTileWidth = 322 + tTileLength = 323 + tTileOffsets = 324 + tTileByteCounts = 325 + + tXResolution = 282 + tYResolution = 283 + tResolutionUnit = 296 + + tPredictor = 317 + tColorMap = 320 + tExtraSamples = 338 + tSampleFormat = 339 +) + +// Compression types (defined in various places in the spec and supplements). +const ( + cNone = 1 + cCCITT = 2 + cG3 = 3 // Group 3 Fax. + cG4 = 4 // Group 4 Fax. + cLZW = 5 + cJPEGOld = 6 // Superseded by cJPEG. + cJPEG = 7 + cDeflate = 8 // zlib compression. + cPackBits = 32773 + cDeflateOld = 32946 // Superseded by cDeflate. +) + +// Photometric interpretation values (see p. 37 of the spec). +const ( + pWhiteIsZero = 0 + pBlackIsZero = 1 + pRGB = 2 + pPaletted = 3 + pTransMask = 4 // transparency mask + pCMYK = 5 + pYCbCr = 6 + pCIELab = 8 +) + +// Values for the tPredictor tag (page 64-65 of the spec). +const ( + prNone = 1 + prHorizontal = 2 +) + +// Values for the tResolutionUnit tag (page 18). +const ( + resNone = 1 + resPerInch = 2 // Dots per inch. + resPerCM = 3 // Dots per centimeter. +) + +// imageMode represents the mode of the image. +type imageMode int + +const ( + mBilevel imageMode = iota + mPaletted + mGray + mGrayInvert + mRGB + mRGBA + mNRGBA + mCMYK +) + +// CompressionType describes the type of compression used in Options. +type CompressionType int + +// Constants for supported compression types. +const ( + Uncompressed CompressionType = iota + Deflate + LZW + CCITTGroup3 + CCITTGroup4 +) + +// specValue returns the compression type constant from the TIFF spec that +// is equivalent to c. +func (c CompressionType) specValue() uint32 { + switch c { + case LZW: + return cLZW + case Deflate: + return cDeflate + case CCITTGroup3: + return cG3 + case CCITTGroup4: + return cG4 + } + return cNone +} diff --git a/vendor/github.com/hhrutter/tiff/reader.go b/vendor/github.com/hhrutter/tiff/reader.go new file mode 100644 index 00000000..ce056666 --- /dev/null +++ b/vendor/github.com/hhrutter/tiff/reader.go @@ -0,0 +1,735 @@ +// Copyright 2011 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. + +// Package tiff is an enhanced version of x/image/tiff. +// +// It uses a consolidated version of compress/lzw (https://github.com/hhrutter/lzw) for compression and also adds support for CMYK. +// +// More information: https://github.com/hhrutter/tiff +package tiff + +import ( + "compress/zlib" + "encoding/binary" + "fmt" + "image" + "image/color" + "io" + "io/ioutil" + "math" + + "github.com/hhrutter/lzw" + "golang.org/x/image/ccitt" +) + +// A FormatError reports that the input is not a valid TIFF image. +type FormatError string + +func (e FormatError) Error() string { + return "tiff: invalid format: " + string(e) +} + +// An UnsupportedError reports that the input uses a valid but +// unimplemented feature. +type UnsupportedError string + +func (e UnsupportedError) Error() string { + return "tiff: unsupported feature: " + string(e) +} + +var errNoPixels = FormatError("not enough pixel data") + +type decoder struct { + r io.ReaderAt + byteOrder binary.ByteOrder + config image.Config + mode imageMode + bpp uint + features map[int][]uint + palette []color.Color + + buf []byte + off int // Current offset in buf. + v uint32 // Buffer value for reading with arbitrary bit depths. + nbits uint // Remaining number of bits in v. +} + +// firstVal returns the first uint of the features entry with the given tag, +// or 0 if the tag does not exist. +func (d *decoder) firstVal(tag int) uint { + f := d.features[tag] + if len(f) == 0 { + return 0 + } + return f[0] +} + +// ifdUint decodes the IFD entry in p, which must be of the Byte, Short +// or Long type, and returns the decoded uint values. +func (d *decoder) ifdUint(p []byte) (u []uint, err error) { + var raw []byte + if len(p) < ifdLen { + return nil, FormatError("bad IFD entry") + } + + datatype := d.byteOrder.Uint16(p[2:4]) + if dt := int(datatype); dt <= 0 || dt >= len(lengths) { + return nil, UnsupportedError("IFD entry datatype") + } + + count := d.byteOrder.Uint32(p[4:8]) + if count > math.MaxInt32/lengths[datatype] { + return nil, FormatError("IFD data too large") + } + if datalen := lengths[datatype] * count; datalen > 4 { + // The IFD contains a pointer to the real value. + raw = make([]byte, datalen) + _, err = d.r.ReadAt(raw, int64(d.byteOrder.Uint32(p[8:12]))) + } else { + raw = p[8 : 8+datalen] + } + if err != nil { + return nil, err + } + + u = make([]uint, count) + switch datatype { + case dtByte: + for i := uint32(0); i < count; i++ { + u[i] = uint(raw[i]) + } + case dtShort: + for i := uint32(0); i < count; i++ { + u[i] = uint(d.byteOrder.Uint16(raw[2*i : 2*(i+1)])) + } + case dtLong: + for i := uint32(0); i < count; i++ { + u[i] = uint(d.byteOrder.Uint32(raw[4*i : 4*(i+1)])) + } + default: + return nil, UnsupportedError("data type") + } + return u, nil +} + +// parseIFD decides whether the the IFD entry in p is "interesting" and +// stows away the data in the decoder. It returns the tag number of the +// entry and an error, if any. +func (d *decoder) parseIFD(p []byte) (int, error) { + tag := d.byteOrder.Uint16(p[0:2]) + switch tag { + case tBitsPerSample, + tExtraSamples, + tPhotometricInterpretation, + tCompression, + tPredictor, + tStripOffsets, + tStripByteCounts, + tRowsPerStrip, + tTileWidth, + tTileLength, + tTileOffsets, + tTileByteCounts, + tImageLength, + tImageWidth, + tFillOrder, + tT4Options, + tT6Options: + val, err := d.ifdUint(p) + if err != nil { + return 0, err + } + d.features[int(tag)] = val + case tColorMap: + val, err := d.ifdUint(p) + if err != nil { + return 0, err + } + numcolors := len(val) / 3 + if len(val)%3 != 0 || numcolors <= 0 || numcolors > 256 { + return 0, FormatError("bad ColorMap length") + } + d.palette = make([]color.Color, numcolors) + for i := 0; i < numcolors; i++ { + d.palette[i] = color.RGBA64{ + uint16(val[i]), + uint16(val[i+numcolors]), + uint16(val[i+2*numcolors]), + 0xffff, + } + } + case tSampleFormat: + // Page 27 of the spec: If the SampleFormat is present and + // the value is not 1 [= unsigned integer data], a Baseline + // TIFF reader that cannot handle the SampleFormat value + // must terminate the import process gracefully. + val, err := d.ifdUint(p) + if err != nil { + return 0, err + } + for _, v := range val { + if v != 1 { + return 0, UnsupportedError("sample format") + } + } + } + return int(tag), nil +} + +// readBits reads n bits from the internal buffer starting at the current offset. +func (d *decoder) readBits(n uint) (v uint32, ok bool) { + for d.nbits < n { + d.v <<= 8 + if d.off >= len(d.buf) { + return 0, false + } + d.v |= uint32(d.buf[d.off]) + d.off++ + d.nbits += 8 + } + d.nbits -= n + rv := d.v >> d.nbits + d.v &^= rv << d.nbits + return rv, true +} + +// flushBits discards the unread bits in the buffer used by readBits. +// It is used at the end of a line. +func (d *decoder) flushBits() { + d.v = 0 + d.nbits = 0 +} + +// minInt returns the smaller of x or y. +func minInt(a, b int) int { + if a <= b { + return a + } + return b +} + +// decode decodes the raw data of an image. +// It reads from d.buf and writes the strip or tile into dst. +func (d *decoder) decode(dst image.Image, xmin, ymin, xmax, ymax int) error { + d.off = 0 + + // Apply horizontal predictor if necessary. + // In this case, p contains the color difference to the preceding pixel. + // See page 64-65 of the spec. + if d.firstVal(tPredictor) == prHorizontal { + switch d.bpp { + case 16: + var off int + n := 2 * len(d.features[tBitsPerSample]) // bytes per sample times samples per pixel + for y := ymin; y < ymax; y++ { + off += n + for x := 0; x < (xmax-xmin-1)*n; x += 2 { + if off+2 > len(d.buf) { + return errNoPixels + } + v0 := d.byteOrder.Uint16(d.buf[off-n : off-n+2]) + v1 := d.byteOrder.Uint16(d.buf[off : off+2]) + d.byteOrder.PutUint16(d.buf[off:off+2], v1+v0) + off += 2 + } + } + case 8: + var off int + n := 1 * len(d.features[tBitsPerSample]) // bytes per sample times samples per pixel + for y := ymin; y < ymax; y++ { + off += n + for x := 0; x < (xmax-xmin-1)*n; x++ { + if off >= len(d.buf) { + return errNoPixels + } + d.buf[off] += d.buf[off-n] + off++ + } + } + case 1: + return UnsupportedError("horizontal predictor with 1 BitsPerSample") + } + } + + rMaxX := minInt(xmax, dst.Bounds().Max.X) + rMaxY := minInt(ymax, dst.Bounds().Max.Y) + switch d.mode { + case mGray, mGrayInvert: + if d.bpp == 16 { + img := dst.(*image.Gray16) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + if d.off+2 > len(d.buf) { + return errNoPixels + } + v := d.byteOrder.Uint16(d.buf[d.off : d.off+2]) + d.off += 2 + if d.mode == mGrayInvert { + v = 0xffff - v + } + img.SetGray16(x, y, color.Gray16{v}) + } + if rMaxX == img.Bounds().Max.X { + d.off += 2 * (xmax - img.Bounds().Max.X) + } + } + } else { + img := dst.(*image.Gray) + max := uint32((1 << d.bpp) - 1) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + v, ok := d.readBits(d.bpp) + if !ok { + return errNoPixels + } + v = v * 0xff / max + if d.mode == mGrayInvert { + v = 0xff - v + } + img.SetGray(x, y, color.Gray{uint8(v)}) + } + d.flushBits() + } + } + case mPaletted: + img := dst.(*image.Paletted) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + v, ok := d.readBits(d.bpp) + if !ok { + return errNoPixels + } + img.SetColorIndex(x, y, uint8(v)) + } + d.flushBits() + } + case mRGB: + if d.bpp == 16 { + img := dst.(*image.RGBA64) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + if d.off+6 > len(d.buf) { + return errNoPixels + } + r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2]) + g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4]) + b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6]) + d.off += 6 + img.SetRGBA64(x, y, color.RGBA64{r, g, b, 0xffff}) + } + } + } else { + img := dst.(*image.RGBA) + for y := ymin; y < rMaxY; y++ { + min := img.PixOffset(xmin, y) + max := img.PixOffset(rMaxX, y) + off := (y - ymin) * (xmax - xmin) * 3 + for i := min; i < max; i += 4 { + if off+3 > len(d.buf) { + return errNoPixels + } + img.Pix[i+0] = d.buf[off+0] + img.Pix[i+1] = d.buf[off+1] + img.Pix[i+2] = d.buf[off+2] + img.Pix[i+3] = 0xff + off += 3 + } + } + } + case mNRGBA: + if d.bpp == 16 { + img := dst.(*image.NRGBA64) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + if d.off+8 > len(d.buf) { + return errNoPixels + } + r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2]) + g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4]) + b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6]) + a := d.byteOrder.Uint16(d.buf[d.off+6 : d.off+8]) + d.off += 8 + img.SetNRGBA64(x, y, color.NRGBA64{r, g, b, a}) + } + } + } else { + img := dst.(*image.NRGBA) + for y := ymin; y < rMaxY; y++ { + min := img.PixOffset(xmin, y) + max := img.PixOffset(rMaxX, y) + i0, i1 := (y-ymin)*(xmax-xmin)*4, (y-ymin+1)*(xmax-xmin)*4 + if i1 > len(d.buf) { + return errNoPixels + } + copy(img.Pix[min:max], d.buf[i0:i1]) + } + } + case mRGBA: + if d.bpp == 16 { + img := dst.(*image.RGBA64) + for y := ymin; y < rMaxY; y++ { + for x := xmin; x < rMaxX; x++ { + if d.off+8 > len(d.buf) { + return errNoPixels + } + r := d.byteOrder.Uint16(d.buf[d.off+0 : d.off+2]) + g := d.byteOrder.Uint16(d.buf[d.off+2 : d.off+4]) + b := d.byteOrder.Uint16(d.buf[d.off+4 : d.off+6]) + a := d.byteOrder.Uint16(d.buf[d.off+6 : d.off+8]) + d.off += 8 + img.SetRGBA64(x, y, color.RGBA64{r, g, b, a}) + } + } + } else { + img := dst.(*image.RGBA) + for y := ymin; y < rMaxY; y++ { + min := img.PixOffset(xmin, y) + max := img.PixOffset(rMaxX, y) + i0, i1 := (y-ymin)*(xmax-xmin)*4, (y-ymin+1)*(xmax-xmin)*4 + if i1 > len(d.buf) { + return errNoPixels + } + copy(img.Pix[min:max], d.buf[i0:i1]) + } + } + case mCMYK: + // d.bpp must be 8 + img := dst.(*image.CMYK) + for y := ymin; y < rMaxY; y++ { + min := img.PixOffset(xmin, y) + max := img.PixOffset(rMaxX, y) + i0, i1 := (y-ymin)*(xmax-xmin)*4, (y-ymin+1)*(xmax-xmin)*4 + if i1 > len(d.buf) { + return errNoPixels + } + copy(img.Pix[min:max], d.buf[i0:i1]) + } + + } + + return nil +} + +func newDecoder(r io.Reader) (*decoder, error) { + d := &decoder{ + r: newReaderAt(r), + features: make(map[int][]uint), + } + + p := make([]byte, 8) + if _, err := d.r.ReadAt(p, 0); err != nil { + return nil, err + } + switch string(p[0:4]) { + case leHeader: + d.byteOrder = binary.LittleEndian + case beHeader: + d.byteOrder = binary.BigEndian + default: + return nil, FormatError("malformed header") + } + + ifdOffset := int64(d.byteOrder.Uint32(p[4:8])) + + // The first two bytes contain the number of entries (12 bytes each). + if _, err := d.r.ReadAt(p[0:2], ifdOffset); err != nil { + return nil, err + } + numItems := int(d.byteOrder.Uint16(p[0:2])) + + // All IFD entries are read in one chunk. + p = make([]byte, ifdLen*numItems) + if _, err := d.r.ReadAt(p, ifdOffset+2); err != nil { + return nil, err + } + + prevTag := -1 + for i := 0; i < len(p); i += ifdLen { + tag, err := d.parseIFD(p[i : i+ifdLen]) + if err != nil { + return nil, err + } + if tag <= prevTag { + return nil, FormatError("tags are not sorted in ascending order") + } + prevTag = tag + } + + d.config.Width = int(d.firstVal(tImageWidth)) + d.config.Height = int(d.firstVal(tImageLength)) + + if _, ok := d.features[tBitsPerSample]; !ok { + // Default is 1 per specification. + d.features[tBitsPerSample] = []uint{1} + } + d.bpp = d.firstVal(tBitsPerSample) + switch d.bpp { + case 0: + return nil, FormatError("BitsPerSample must not be 0") + case 1, 8, 16: + // Nothing to do, these are accepted by this implementation. + default: + return nil, UnsupportedError(fmt.Sprintf("BitsPerSample of %v", d.bpp)) + } + + // Determine the image mode. + switch d.firstVal(tPhotometricInterpretation) { + case pRGB: + if d.bpp == 16 { + for _, b := range d.features[tBitsPerSample] { + if b != 16 { + return nil, FormatError("wrong number of samples for 16bit RGB") + } + } + } else { + for _, b := range d.features[tBitsPerSample] { + if b != 8 { + return nil, FormatError("wrong number of samples for 8bit RGB") + } + } + } + // RGB images normally have 3 samples per pixel. + // If there are more, ExtraSamples (p. 31-32 of the spec) + // gives their meaning (usually an alpha channel). + // + // This implementation does not support extra samples + // of an unspecified type. + switch len(d.features[tBitsPerSample]) { + case 3: + d.mode = mRGB + if d.bpp == 16 { + d.config.ColorModel = color.RGBA64Model + } else { + d.config.ColorModel = color.RGBAModel + } + case 4: + switch d.firstVal(tExtraSamples) { + case 1: + d.mode = mRGBA + if d.bpp == 16 { + d.config.ColorModel = color.RGBA64Model + } else { + d.config.ColorModel = color.RGBAModel + } + case 2: + d.mode = mNRGBA + if d.bpp == 16 { + d.config.ColorModel = color.NRGBA64Model + } else { + d.config.ColorModel = color.NRGBAModel + } + default: + return nil, FormatError("wrong number of samples for RGB") + } + default: + return nil, FormatError("wrong number of samples for RGB") + } + case pPaletted: + d.mode = mPaletted + d.config.ColorModel = color.Palette(d.palette) + case pWhiteIsZero: + d.mode = mGrayInvert + if d.bpp == 16 { + d.config.ColorModel = color.Gray16Model + } else { + d.config.ColorModel = color.GrayModel + } + case pBlackIsZero: + d.mode = mGray + if d.bpp == 16 { + d.config.ColorModel = color.Gray16Model + } else { + d.config.ColorModel = color.GrayModel + } + case pCMYK: + d.mode = mCMYK + if d.bpp == 16 { + return nil, UnsupportedError(fmt.Sprintf("CMYK BitsPerSample of %v", d.bpp)) + } + d.config.ColorModel = color.CMYKModel + + default: + return nil, UnsupportedError("color model") + } + + return d, nil +} + +// DecodeConfig returns the color model and dimensions of a TIFF image without +// decoding the entire image. +func DecodeConfig(r io.Reader) (image.Config, error) { + d, err := newDecoder(r) + if err != nil { + return image.Config{}, err + } + return d.config, nil +} + +func ccittFillOrder(tiffFillOrder uint) ccitt.Order { + if tiffFillOrder == 2 { + return ccitt.LSB + } + return ccitt.MSB +} + +// Decode reads a TIFF image from r and returns it as an image.Image. +// The type of Image returned depends on the contents of the TIFF. +func Decode(r io.Reader) (img image.Image, err error) { + d, err := newDecoder(r) + if err != nil { + return + } + + blockPadding := false + blockWidth := d.config.Width + blockHeight := d.config.Height + blocksAcross := 1 + blocksDown := 1 + + if d.config.Width == 0 { + blocksAcross = 0 + } + if d.config.Height == 0 { + blocksDown = 0 + } + + var blockOffsets, blockCounts []uint + + if int(d.firstVal(tTileWidth)) != 0 { + blockPadding = true + + blockWidth = int(d.firstVal(tTileWidth)) + blockHeight = int(d.firstVal(tTileLength)) + + if blockWidth != 0 { + blocksAcross = (d.config.Width + blockWidth - 1) / blockWidth + } + if blockHeight != 0 { + blocksDown = (d.config.Height + blockHeight - 1) / blockHeight + } + + blockCounts = d.features[tTileByteCounts] + blockOffsets = d.features[tTileOffsets] + + } else { + if int(d.firstVal(tRowsPerStrip)) != 0 { + blockHeight = int(d.firstVal(tRowsPerStrip)) + } + + if blockHeight != 0 { + blocksDown = (d.config.Height + blockHeight - 1) / blockHeight + } + + blockOffsets = d.features[tStripOffsets] + blockCounts = d.features[tStripByteCounts] + } + + // Check if we have the right number of strips/tiles, offsets and counts. + if n := blocksAcross * blocksDown; len(blockOffsets) < n || len(blockCounts) < n { + return nil, FormatError("inconsistent header") + } + + imgRect := image.Rect(0, 0, d.config.Width, d.config.Height) + switch d.mode { + case mGray, mGrayInvert: + if d.bpp == 16 { + img = image.NewGray16(imgRect) + } else { + img = image.NewGray(imgRect) + } + case mPaletted: + img = image.NewPaletted(imgRect, d.palette) + case mNRGBA: + if d.bpp == 16 { + img = image.NewNRGBA64(imgRect) + } else { + img = image.NewNRGBA(imgRect) + } + case mRGB, mRGBA: + if d.bpp == 16 { + img = image.NewRGBA64(imgRect) + } else { + img = image.NewRGBA(imgRect) + } + case mCMYK: + img = image.NewCMYK(imgRect) + } + + for i := 0; i < blocksAcross; i++ { + blkW := blockWidth + if !blockPadding && i == blocksAcross-1 && d.config.Width%blockWidth != 0 { + blkW = d.config.Width % blockWidth + } + for j := 0; j < blocksDown; j++ { + blkH := blockHeight + if !blockPadding && j == blocksDown-1 && d.config.Height%blockHeight != 0 { + blkH = d.config.Height % blockHeight + } + offset := int64(blockOffsets[j*blocksAcross+i]) + n := int64(blockCounts[j*blocksAcross+i]) + // LSBToMSB := d.firstVal(tFillOrder) == 2 + // order := ccitt.MSB + // if LSBToMSB { + // order = ccitt.LSB + // } + switch d.firstVal(tCompression) { + + // According to the spec, Compression does not have a default value, + // but some tools interpret a missing Compression value as none so we do + // the same. + case cNone, 0: + if b, ok := d.r.(*buffer); ok { + d.buf, err = b.Slice(int(offset), int(n)) + } else { + d.buf = make([]byte, n) + _, err = d.r.ReadAt(d.buf, offset) + } + case cG3: + inv := d.firstVal(tPhotometricInterpretation) == pWhiteIsZero + order := ccittFillOrder(d.firstVal(tFillOrder)) + r := ccitt.NewReader(io.NewSectionReader(d.r, offset, n), order, ccitt.Group3, blkW, blkH, &ccitt.Options{Invert: inv, Align: false}) + d.buf, err = ioutil.ReadAll(r) + case cG4: + inv := d.firstVal(tPhotometricInterpretation) == pWhiteIsZero + order := ccittFillOrder(d.firstVal(tFillOrder)) + r := ccitt.NewReader(io.NewSectionReader(d.r, offset, n), order, ccitt.Group4, blkW, blkH, &ccitt.Options{Invert: inv, Align: false}) + d.buf, err = ioutil.ReadAll(r) + case cLZW: + r := lzw.NewReader(io.NewSectionReader(d.r, offset, n), true) + d.buf, err = ioutil.ReadAll(r) + r.Close() + case cDeflate, cDeflateOld: + var r io.ReadCloser + r, err = zlib.NewReader(io.NewSectionReader(d.r, offset, n)) + if err != nil { + return nil, err + } + d.buf, err = ioutil.ReadAll(r) + r.Close() + case cPackBits: + d.buf, err = unpackBits(io.NewSectionReader(d.r, offset, n)) + default: + err = UnsupportedError(fmt.Sprintf("compression value %d", d.firstVal(tCompression))) + } + if err != nil { + return nil, err + } + + xmin := i * blockWidth + ymin := j * blockHeight + xmax := xmin + blkW + ymax := ymin + blkH + err = d.decode(img, xmin, ymin, xmax, ymax) + if err != nil { + return nil, err + } + } + } + return +} + +func init() { + image.RegisterFormat("tiff", leHeader, Decode, DecodeConfig) + image.RegisterFormat("tiff", beHeader, Decode, DecodeConfig) +} diff --git a/vendor/github.com/hhrutter/tiff/writer.go b/vendor/github.com/hhrutter/tiff/writer.go new file mode 100644 index 00000000..47d71fc7 --- /dev/null +++ b/vendor/github.com/hhrutter/tiff/writer.go @@ -0,0 +1,482 @@ +// 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. + +package tiff + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "fmt" + "image" + "io" + "sort" + + "github.com/hhrutter/lzw" +) + +// The TIFF format allows to choose the order of the different elements freely. +// The basic structure of a TIFF file written by this package is: +// +// 1. Header (8 bytes). +// 2. Image data. +// 3. Image File Directory (IFD). +// 4. "Pointer area" for larger entries in the IFD. + +// We only write little-endian TIFF files. +var enc = binary.LittleEndian + +// An ifdEntry is a single entry in an Image File Directory. +// A value of type dtRational is composed of two 32-bit values, +// thus data contains two uints (numerator and denominator) for a single number. +type ifdEntry struct { + tag int + datatype int + data []uint32 +} + +func (e ifdEntry) putData(p []byte) { + for _, d := range e.data { + switch e.datatype { + case dtByte, dtASCII: + p[0] = byte(d) + p = p[1:] + case dtShort: + enc.PutUint16(p, uint16(d)) + p = p[2:] + case dtLong, dtRational: + enc.PutUint32(p, uint32(d)) + p = p[4:] + } + } +} + +type byTag []ifdEntry + +func (d byTag) Len() int { return len(d) } +func (d byTag) Less(i, j int) bool { return d[i].tag < d[j].tag } +func (d byTag) Swap(i, j int) { d[i], d[j] = d[j], d[i] } + +func encodeGray(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error { + if !predictor { + return writePix(w, pix, dy, dx, stride) + } + buf := make([]byte, dx) + for y := 0; y < dy; y++ { + min := y*stride + 0 + max := y*stride + dx + off := 0 + var v0 uint8 + for i := min; i < max; i++ { + v1 := pix[i] + buf[off] = v1 - v0 + v0 = v1 + off++ + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +func encodeGray16(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error { + buf := make([]byte, dx*2) + for y := 0; y < dy; y++ { + min := y*stride + 0 + max := y*stride + dx*2 + off := 0 + var v0 uint16 + for i := min; i < max; i += 2 { + // An image.Gray16's Pix is in big-endian order. + v1 := uint16(pix[i])<<8 | uint16(pix[i+1]) + if predictor { + v0, v1 = v1, v1-v0 + } + // We only write little-endian TIFF files. + buf[off+0] = byte(v1) + buf[off+1] = byte(v1 >> 8) + off += 2 + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +func encodeRGBA(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error { + if !predictor { + return writePix(w, pix, dy, dx*4, stride) + } + buf := make([]byte, dx*4) + for y := 0; y < dy; y++ { + min := y*stride + 0 + max := y*stride + dx*4 + off := 0 + var r0, g0, b0, a0 uint8 + for i := min; i < max; i += 4 { + r1, g1, b1, a1 := pix[i+0], pix[i+1], pix[i+2], pix[i+3] + buf[off+0] = r1 - r0 + buf[off+1] = g1 - g0 + buf[off+2] = b1 - b0 + buf[off+3] = a1 - a0 + off += 4 + r0, g0, b0, a0 = r1, g1, b1, a1 + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +func encodeRGBA64(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error { + buf := make([]byte, dx*8) + for y := 0; y < dy; y++ { + min := y*stride + 0 + max := y*stride + dx*8 + off := 0 + var r0, g0, b0, a0 uint16 + for i := min; i < max; i += 8 { + // An image.RGBA64's Pix is in big-endian order. + r1 := uint16(pix[i+0])<<8 | uint16(pix[i+1]) + g1 := uint16(pix[i+2])<<8 | uint16(pix[i+3]) + b1 := uint16(pix[i+4])<<8 | uint16(pix[i+5]) + a1 := uint16(pix[i+6])<<8 | uint16(pix[i+7]) + if predictor { + r0, r1 = r1, r1-r0 + g0, g1 = g1, g1-g0 + b0, b1 = b1, b1-b0 + a0, a1 = a1, a1-a0 + } + // We only write little-endian TIFF files. + buf[off+0] = byte(r1) + buf[off+1] = byte(r1 >> 8) + buf[off+2] = byte(g1) + buf[off+3] = byte(g1 >> 8) + buf[off+4] = byte(b1) + buf[off+5] = byte(b1 >> 8) + buf[off+6] = byte(a1) + buf[off+7] = byte(a1 >> 8) + off += 8 + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +func encodeCMYK(w io.Writer, pix []uint8, dx, dy, stride int, predictor bool) error { + if !predictor { + return writePix(w, pix, dy, dx*4, stride) + } + buf := make([]byte, dx*4) + for y := 0; y < dy; y++ { + min := y*stride + 0 + max := y*stride + dx*4 + off := 0 + var c0, m0, y0, k0 uint8 + for i := min; i < max; i += 4 { + c1, m1, y1, k1 := pix[i+0], pix[i+1], pix[i+2], pix[i+3] + buf[off+0] = c1 - c0 + buf[off+1] = m1 - m0 + buf[off+2] = y1 - y0 + buf[off+3] = k1 - k0 + off += 4 + c0, m0, y0, k0 = c1, m1, y1, k1 + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +func encode(w io.Writer, m image.Image, predictor bool) error { + bounds := m.Bounds() + buf := make([]byte, 4*bounds.Dx()) + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + off := 0 + if predictor { + var r0, g0, b0, a0 uint8 + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r, g, b, a := m.At(x, y).RGBA() + r1 := uint8(r >> 8) + g1 := uint8(g >> 8) + b1 := uint8(b >> 8) + a1 := uint8(a >> 8) + buf[off+0] = r1 - r0 + buf[off+1] = g1 - g0 + buf[off+2] = b1 - b0 + buf[off+3] = a1 - a0 + off += 4 + r0, g0, b0, a0 = r1, g1, b1, a1 + } + } else { + for x := bounds.Min.X; x < bounds.Max.X; x++ { + r, g, b, a := m.At(x, y).RGBA() + buf[off+0] = uint8(r >> 8) + buf[off+1] = uint8(g >> 8) + buf[off+2] = uint8(b >> 8) + buf[off+3] = uint8(a >> 8) + off += 4 + } + } + if _, err := w.Write(buf); err != nil { + return err + } + } + return nil +} + +// writePix writes the internal byte array of an image to w. It is less general +// but much faster then encode. writePix is used when pix directly +// corresponds to one of the TIFF image types. +func writePix(w io.Writer, pix []byte, nrows, length, stride int) error { + if length == stride { + _, err := w.Write(pix[:nrows*length]) + return err + } + for ; nrows > 0; nrows-- { + if _, err := w.Write(pix[:length]); err != nil { + return err + } + pix = pix[stride:] + } + return nil +} + +func writeIFD(w io.Writer, ifdOffset int, d []ifdEntry) error { + var buf [ifdLen]byte + // Make space for "pointer area" containing IFD entry data + // longer than 4 bytes. + parea := make([]byte, 1024) + pstart := ifdOffset + ifdLen*len(d) + 6 + var o int // Current offset in parea. + + // The IFD has to be written with the tags in ascending order. + sort.Sort(byTag(d)) + + // Write the number of entries in this IFD. + if err := binary.Write(w, enc, uint16(len(d))); err != nil { + return err + } + for _, ent := range d { + enc.PutUint16(buf[0:2], uint16(ent.tag)) + enc.PutUint16(buf[2:4], uint16(ent.datatype)) + count := uint32(len(ent.data)) + if ent.datatype == dtRational { + count /= 2 + } + enc.PutUint32(buf[4:8], count) + datalen := int(count * lengths[ent.datatype]) + if datalen <= 4 { + ent.putData(buf[8:12]) + } else { + if (o + datalen) > len(parea) { + newlen := len(parea) + 1024 + for (o + datalen) > newlen { + newlen += 1024 + } + newarea := make([]byte, newlen) + copy(newarea, parea) + parea = newarea + } + ent.putData(parea[o : o+datalen]) + enc.PutUint32(buf[8:12], uint32(pstart+o)) + o += datalen + } + if _, err := w.Write(buf[:]); err != nil { + return err + } + } + // The IFD ends with the offset of the next IFD in the file, + // or zero if it is the last one (page 14). + if err := binary.Write(w, enc, uint32(0)); err != nil { + return err + } + _, err := w.Write(parea[:o]) + return err +} + +// Options are the encoding parameters. +type Options struct { + // Compression is the type of compression used. + Compression CompressionType + // Predictor determines whether a differencing predictor is used; + // if true, instead of each pixel's color, the color difference to the + // preceding one is saved. This improves the compression for certain + // types of images and compressors. For example, it works well for + // photos with Deflate compression. + Predictor bool +} + +// Encode writes the image m to w. opt determines the options used for +// encoding, such as the compression type. If opt is nil, an uncompressed +// image is written. +func Encode(w io.Writer, m image.Image, opt *Options) error { + d := m.Bounds().Size() + + compression := uint32(cNone) + predictor := false + if opt != nil { + compression = opt.Compression.specValue() + // The TIFF 6.0 spec (June,1992) says the predictor field is only to be used with LZW. (See page 64). + // Yet this TIFF writer also allows prediction for Deflate compression. + // This makes sense as Deflate is supposedly the successor to LWZ. + // Also both PNG and PDF use Deflate with predictors. + predictor = opt.Predictor && compression == cLZW || compression == cDeflate + } + + _, err := io.WriteString(w, leHeader) + if err != nil { + return err + } + + // Compressed data is written into a buffer first, so that we + // know the compressed size. + var buf bytes.Buffer + // dst holds the destination for the pixel data of the image -- + // either w or a writer to buf. + var dst io.Writer + // imageLen is the length of the pixel data in bytes. + // The offset of the IFD is imageLen + 8 header bytes. + var imageLen int + + switch compression { + case cNone: + dst = w + // Write IFD offset before outputting pixel data. + switch m.(type) { + case *image.Paletted: + imageLen = d.X * d.Y * 1 + case *image.Gray: + imageLen = d.X * d.Y * 1 + case *image.Gray16: + imageLen = d.X * d.Y * 2 + case *image.RGBA64: + imageLen = d.X * d.Y * 8 + case *image.NRGBA64: + imageLen = d.X * d.Y * 8 + case *image.CMYK: + imageLen = d.X * d.Y * 4 + default: + imageLen = d.X * d.Y * 4 + } + err = binary.Write(w, enc, uint32(imageLen+8)) + case cLZW: + dst = lzw.NewWriter(&buf, true) + case cDeflate: + dst = zlib.NewWriter(&buf) + default: + err = UnsupportedError(fmt.Sprintf("compression value %d", compression)) + } + + if err != nil { + return err + } + + pr := uint32(prNone) + photometricInterpretation := uint32(pRGB) + samplesPerPixel := uint32(4) + bitsPerSample := []uint32{8, 8, 8, 8} + extraSamples := uint32(0) + colorMap := []uint32{} + + if predictor { + pr = prHorizontal + } + switch m := m.(type) { + case *image.Paletted: + photometricInterpretation = pPaletted + samplesPerPixel = 1 + bitsPerSample = []uint32{8} + colorMap = make([]uint32, 256*3) + for i := 0; i < 256 && i < len(m.Palette); i++ { + r, g, b, _ := m.Palette[i].RGBA() + colorMap[i+0*256] = uint32(r) + colorMap[i+1*256] = uint32(g) + colorMap[i+2*256] = uint32(b) + } + err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.Gray: + photometricInterpretation = pBlackIsZero + samplesPerPixel = 1 + bitsPerSample = []uint32{8} + err = encodeGray(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.Gray16: + photometricInterpretation = pBlackIsZero + samplesPerPixel = 1 + bitsPerSample = []uint32{16} + err = encodeGray16(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.NRGBA: + extraSamples = 2 // Unassociated alpha. + err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.NRGBA64: + extraSamples = 2 // Unassociated alpha. + bitsPerSample = []uint32{16, 16, 16, 16} + err = encodeRGBA64(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.RGBA: + extraSamples = 1 // Associated alpha. + err = encodeRGBA(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.RGBA64: + extraSamples = 1 // Associated alpha. + bitsPerSample = []uint32{16, 16, 16, 16} + err = encodeRGBA64(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + case *image.CMYK: + photometricInterpretation = uint32(pCMYK) + samplesPerPixel = uint32(4) + bitsPerSample = []uint32{8, 8, 8, 8} + err = encodeCMYK(dst, m.Pix, d.X, d.Y, m.Stride, predictor) + default: + extraSamples = 1 // Associated alpha. + err = encode(dst, m, predictor) + } + if err != nil { + return err + } + + if compression != cNone { + if err = dst.(io.Closer).Close(); err != nil { + return err + } + imageLen = buf.Len() + if err = binary.Write(w, enc, uint32(imageLen+8)); err != nil { + return err + } + if _, err = buf.WriteTo(w); err != nil { + return err + } + } + + ifd := []ifdEntry{ + {tImageWidth, dtShort, []uint32{uint32(d.X)}}, + {tImageLength, dtShort, []uint32{uint32(d.Y)}}, + {tBitsPerSample, dtShort, bitsPerSample}, + {tCompression, dtShort, []uint32{compression}}, + {tPhotometricInterpretation, dtShort, []uint32{photometricInterpretation}}, + {tStripOffsets, dtLong, []uint32{8}}, + {tSamplesPerPixel, dtShort, []uint32{samplesPerPixel}}, + {tRowsPerStrip, dtShort, []uint32{uint32(d.Y)}}, + {tStripByteCounts, dtLong, []uint32{uint32(imageLen)}}, + // There is currently no support for storing the image + // resolution, so give a bogus value of 72x72 dpi. + {tXResolution, dtRational, []uint32{72, 1}}, + {tYResolution, dtRational, []uint32{72, 1}}, + {tResolutionUnit, dtShort, []uint32{resPerInch}}, + } + if pr != prNone { + ifd = append(ifd, ifdEntry{tPredictor, dtShort, []uint32{pr}}) + } + if len(colorMap) != 0 { + ifd = append(ifd, ifdEntry{tColorMap, dtShort, colorMap}) + } + if extraSamples > 0 { + ifd = append(ifd, ifdEntry{tExtraSamples, dtShort, []uint32{extraSamples}}) + } + + return writeIFD(w, imageLen+8, ifd) +} diff --git a/vendor/github.com/pdfcpu/pdfcpu/LICENSE.txt b/vendor/github.com/pdfcpu/pdfcpu/LICENSE.txt new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/metrics.go b/vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/metrics.go new file mode 100644 index 00000000..19102b5b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/standard.go b/vendor/github.com/pdfcpu/pdfcpu/internal/corefont/metrics/standard.go new file mode 100644 index 00000000..2bcf9b86 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ascii85Decode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ascii85Decode.go new file mode 100644 index 00000000..3f195e07 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/asciiHexDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/asciiHexDecode.go new file mode 100644 index 00000000..5d0945a2 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ccittDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/ccittDecode.go new file mode 100644 index 00000000..4f93e759 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/dctDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/dctDecode.go new file mode 100644 index 00000000..593c3ccc --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/filter.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/filter.go new file mode 100644 index 00000000..d8d68ebe --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/filter.go @@ -0,0 +1,125 @@ +/* +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 SupportsDecodeParms(f string) bool { + return f == CCITTFax || f == LZW || f == Flate +} + +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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/flateDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/flateDecode.go new file mode 100644 index 00000000..532c0d16 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/lzwDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/lzwDecode.go new file mode 100644 index 00000000..ac5e8690 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/paeth.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/paeth.go new file mode 100644 index 00000000..c0223328 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/runLengthDecode.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/filter/runLengthDecode.go new file mode 100644 index 00000000..2cbb0bcf --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/font/install.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/font/install.go new file mode 100644 index 00000000..10796c96 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/font/metrics.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/font/metrics.go new file mode 100644 index 00000000..720b8872 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/log/log.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/log/log.go new file mode 100644 index 00000000..a621680e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/annotation.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/annotation.go new file mode 100644 index 00000000..ae7d7301 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/booklet.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/booklet.go new file mode 100644 index 00000000..d9a45903 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/booklet.go @@ -0,0 +1,609 @@ +/* + 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 = true + 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 special cases + if nup.IsBooklet() && val == 6 && nup.IsTopFoldBinding() { + // You can't top fold a 6up with 3 rows. + return nup, fmt.Errorf("pdfcpu booklet: n=6 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] +} + +type pageNumberFunction func(inputPageNr int, pageCount int, pageNumbers []int, nup *model.NUp) (int, bool) + +func nup2OutputPageNr(inputPageNr, inputPageCount int, pageNumbers []int, _ *model.NUp) (int, bool) { + // (output page, input page) = [(1,n), (2,1), (3, n-1), (4, 2), (5, n-2), (6, 3), ...] + 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) + + var pageNumberFn pageNumberFunction + switch nup.BookletType { + case model.Booklet, model.BookletAdvanced: + switch nup.N() { + case 2: + pageNumberFn = nup2OutputPageNr + case 4: + pageNumberFn = nup4OutputPageNr + case 6: + pageNumberFn = nupLRTBOutputPageNr + case 8: + if nup.BookletBinding == model.ShortEdge { + pageNumberFn = nupLRTBOutputPageNr + } else { // long edge + pageNumberFn = nup8OutputPageNr + } + } + case model.BookletPerfectBound: + pageNumberFn = nupPerfectBound + } + + for i := 0; i < pageCount; i++ { + pageNr, rotate := pageNumberFn(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/bookmark.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/bookmark.go new file mode 100644 index 00000000..145a682a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color/color.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/color/color.go new file mode 100644 index 00000000..45c29607 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createAnnotations.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createAnnotations.go new file mode 100644 index 00000000..cfb3b974 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createRenditions.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createRenditions.go new file mode 100644 index 00000000..f49a9d2f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createTestPDF.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/createTestPDF.go new file mode 100644 index 00000000..d2a27185 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/crypto.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/crypto.go new file mode 100644 index 00000000..8cdf9aea --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/crypto.go @@ -0,0 +1,1822 @@ +/* +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.UPDATEIMAGES: {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}, + model.ZOOM: {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.XRefTable.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.XRefTable.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.XRefTable.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.XRefTable.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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/cut.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/cut.go new file mode 100644 index 00000000..90f95059 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/doc.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/doc.go new file mode 100644 index 00000000..0384d9ee --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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 list, reset 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, extract, update images + 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 +*/ +package pdfcpu diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw/draw.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/draw/draw.go new file mode 100644 index 00000000..959c44b4 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/extract.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/extract.go new file mode 100644 index 00000000..6b2fe021 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.LZW, 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[pageNr-1], 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font/fontDict.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/font/fontDict.go new file mode 100644 index 00000000..5f4dc4b3 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format/format.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/format/format.go new file mode 100644 index 00000000..0591f355 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/iccProfile.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/iccProfile.go new file mode 100644 index 00000000..6ec8b84b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/image.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/image.go new file mode 100644 index 00000000..4fa271b5 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/image.go @@ -0,0 +1,392 @@ +/* +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" + "io" + "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" + "github.com/pkg/errors" +) + +// 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(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(mm, maxLen) + if err != nil { + return nil, err + } + + s := fmt.Sprintf("%d images available", j) + + if j > 0 { + s += fmt.Sprintf(" (%s)", types.ByteSize(size)) + } + + return append([]string{s}, 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) + outFile := filepath.Join(outDir, f) + log.CLI.Printf("writing %s\n", outFile) + return WriteReader(outFile, img) + } +} + +func validateImageDimensions(ctx *model.Context, objNr, w, h int) error { + imgObj := ctx.Optimize.ImageObjects[objNr] + if imgObj == nil { + return errors.Errorf("pdfcpu: unknown image object for objNr=%d", objNr) + } + + d := imgObj.ImageDict + + width := d.IntEntry("Width") + height := d.IntEntry("Height") + + if width == nil || height == nil { + return errors.New("pdfcpu: corrupt image dict") + } + + if *width != w || *height != h { + return errors.Errorf("pdfcpu: invalid image dimensions, want(%d,%d), got(%d,%d)", w, h, *width, *height) + } + + return nil +} + +// UpdateImagesByObjNr replaces an XObject. +func UpdateImagesByObjNr(ctx *model.Context, rd io.Reader, objNr int) error { + + sd, w, h, err := model.CreateImageStreamDict(ctx.XRefTable, rd, false, false) + if err != nil { + return err + } + + if err := validateImageDimensions(ctx, objNr, w, h); err != nil { + return err + } + + genNr := 0 + entry, ok := ctx.FindTableEntry(objNr, genNr) + if !ok { + errors.Errorf("pdfcpu: invalid objNr=%d", objNr) + } + + entry.Object = *sd + + return nil +} + +func isInheritedXObjectResource(inhRes types.Dict, id string) bool { + if inhRes == nil { + return false + } + + d := inhRes.DictEntry("XObject") + if d == nil { + return false + } + + for resId := range d { + if resId == id { + return true + } + } + + return false +} + +// UpdateImagesByPageNrAndId replaces the XObject referenced by pageNr and id. +func UpdateImagesByPageNrAndId(ctx *model.Context, rd io.Reader, pageNr int, id string) error { + + imgIndRef, w, h, err := model.CreateImageResource(ctx.XRefTable, rd, false, false) + if err != nil { + return err + } + + d, _, inhPAttrs, err := ctx.PageDict(pageNr, false) + if err != nil { + return err + } + + obj, found := d.Find("Resources") + if !found { + if isInheritedXObjectResource(inhPAttrs.Resources, id) { + d1 := types.NewDict() + d1[id] = *imgIndRef + d2 := types.NewDict() + d2["XObject"] = d1 + d["Resources"] = d2 + return nil + } + return errors.Errorf("pdfcpu: page %d: unknown resource %s\n", pageNr, id) + } + + resDict, err := ctx.DereferenceDict(obj) + if err != nil { + return err + } + + obj1, ok := resDict.Find("XObject") + if !ok { + if isInheritedXObjectResource(inhPAttrs.Resources, id) { + d := types.NewDict() + d[id] = *imgIndRef + resDict["XObject"] = d + return nil + } + return errors.Errorf("pdfcpu: page %d: unknown resource %s\n", pageNr, id) + } + + imgResDict, err := ctx.DereferenceDict(obj1) + if err != nil { + return err + } + + for resId, indRef := range imgResDict { + if resId == id { + + ir := indRef.(types.IndirectRef) + if err := validateImageDimensions(ctx, ir.ObjectNumber.Value(), w, h); err != nil { + return err + } + + imgResDict[id] = *imgIndRef + return nil + } + } + + if isInheritedXObjectResource(inhPAttrs.Resources, id) { + imgResDict[id] = *imgIndRef + return nil + } + + return errors.Errorf("pdfcpu: page %d: unknown resource %s\n", pageNr, id) +} diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/importImage.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/importImage.go new file mode 100644 index 00000000..baf74480 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/info.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/info.go new file mode 100644 index 00000000..980d0b15 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.XRefTable.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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/keyword.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/keyword.go new file mode 100644 index 00000000..07b70847 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix/matrix.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/matrix/matrix.go new file mode 100644 index 00000000..5829c4dd --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/merge.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/merge.go new file mode 100644 index 00000000..cd9f05a5 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/migrate.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/migrate.go new file mode 100644 index 00000000..91ec23f9 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/annotation.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/annotation.go new file mode 100644 index 00000000..312cdb8e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/annotation.go @@ -0,0 +1,1674 @@ +/* +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.EscapedUTF16String(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.EscapedUTF16String(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.EscapedUTF16String(ann.RC) + if err != nil { + return nil, err + } + d.InsertString("RC", *s) + } + + d.InsertString("CreationDate", ann.CreationDate) + + if ann.Subj != "" { + s, err := types.EscapedUTF16String(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.EscapedUTF16String(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 +} + +func (ann LineAnnotation) validateLeaderLineAttrs() error { + if ann.LeaderLineExtensionLength < 0 { + return errors.New("pdfcpu: LineAnnotation leader line extension length must not be negative.") + } + + if ann.LeaderLineExtensionLength > 0 && ann.LeaderLineLength == 0 { + return errors.New("pdfcpu: LineAnnotation leader line length missing.") + } + + if ann.LeaderLineOffset < 0 { + return errors.New("pdfcpu: LineAnnotation leader line offset must not be negative.") + } + + return nil +} + +// 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 err := ann.validateLeaderLineAttrs(); err != nil { + return nil, err + } + + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/attach.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/attach.go new file mode 100644 index 00000000..8be0fd9a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/attach.go @@ -0,0 +1,429 @@ +/* +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/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) error { + fpl := sd.FilterPipeline + + if fpl == nil { + sd.Content = sd.Raw + return nil + } + + 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) + + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/booklet.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/booklet.go new file mode 100644 index 00000000..91a6dd4b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/booklet.go @@ -0,0 +1,236 @@ +/* + 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: + if nup.BookletBinding == LongEdge { + // Also has cuts in the center row & column. + return cut, cut + } else { + // short edge has the fold in the center col. cut on each row + return cut, fold + } + } + 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: + if nup.BookletBinding == LongEdge { + // 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) + } else { + // short edge: cuts on rows + for i := 1; i < 4; i++ { + drawGuideHorizontal(w, height*float64(i)/4, width, cut, nup, mb, fm) + } + } + } + } + if vert != none { + drawGuideVertical(w, width/2, height, vert, nup, mb, fm) + } + return fm +} diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/box.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/box.go new file mode 100644 index 00000000..1e0d042f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/colorSpace.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/colorSpace.go new file mode 100644 index 00000000..62e5fba4 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/configuration.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/configuration.go new file mode 100644 index 00000000..d2093342 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/configuration.go @@ -0,0 +1,544 @@ +/* +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 + UPDATEIMAGES + 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 + + CreationDate string + + Version 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 + + // Internet availability. + Offline bool + + // HTTP timeout in seconds. + Timeout int +} + +// 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, override bool) error { + f, err := os.Open(path) + if err != nil || override { + f.Close() + + s := fmt.Sprintf(` +############################# +# Default configuration # +############################# + +# Creation date +created: %s + +# version (Do not edit!) +version: %s + +`, + time.Now().Format("2006-01-02 15:04"), + VersionStr) + + 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, override bool) 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"), override); 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{ + CreationDate: time.Now().Format("2006-01-02 15:04"), + Version: VersionStr, + 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, + Offline: false, + Timeout: 5, + } +} + +func ResetConfig() error { + path, err := os.UserConfigDir() + if err != nil { + path = os.TempDir() + } + return EnsureDefaultConfigAt(path, true) +} + +// 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, false); err == nil { + c := *loadedDefaultConfig + return &c + } + fmt.Fprintf(os.Stderr, "pdfcpu: config 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"+ + "CreationDate: %s\n"+ + "Version: %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"+ + "Offline %t\n"+ + "Timeout %d\n", + path, + c.CreationDate, + c.Version, + 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, + c.Offline, + c.Timeout, + ) +} + +// 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/context.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/context.go new file mode 100644 index 00000000..357d35c0 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/context.go @@ -0,0 +1,648 @@ +/* +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] + resName, ok := imageObject.ResourceNames[i] + if ok { + logStr = append(logStr, fmt.Sprintf("#%-6d %s\n", objectNumber, resName)) + } + } + } + + // 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/cut.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/cut.go new file mode 100644 index 00000000..78690d28 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/dereference.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/dereference.go new file mode 100644 index 00000000..049aef32 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/dereference.go @@ -0,0 +1,470 @@ +/* +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 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/destination.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/destination.go new file mode 100644 index 00000000..405bcf20 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/document.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/document.go new file mode 100644 index 00000000..a0e74dbf --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/equal.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/equal.go new file mode 100644 index 00000000..1cb70e68 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/image.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/image.go new file mode 100644 index 00000000..4a164f7e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/image.go @@ -0,0 +1,627 @@ +/* +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 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(b) + draw.Draw(m, m.Bounds(), img, b.Min, draw.Src) + return m +} + +func convertNYCbCrAToRGBA(img *image.NYCbCrA) *image.RGBA { + b := img.Bounds() + m := image.NewRGBA(b) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + ycbr := img.YCbCrAt(x, y) + stride := img.Bounds().Dx() + alphaOffset := (y-b.Min.Y)*stride + (x - b.Min.X) + alpha := img.A[alphaOffset] + r, g, b := color.YCbCrToRGB(ycbr.Y, ycbr.Cb, ycbr.Cr) + m.Set(x, y, color.RGBA{R: r, G: g, B: b, A: alpha}) + } + } + 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 := 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: + cs = DeviceRGBCS + bpc = 8 + buf, sm = writeRGBAImageBuf(convertNYCbCrAToRGBA(img)) + + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/metadata.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/metadata.go new file mode 100644 index 00000000..aae62cb0 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nameTree.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nameTree.go new file mode 100644 index 00000000..7429ecbf --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nup.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/nup.go new file mode 100644 index 00000000..330a0048 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/page.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/page.go new file mode 100644 index 00000000..f11a4fa0 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parse.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parse.go new file mode 100644 index 00000000..04312bed --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parse.go @@ -0,0 +1,1297 @@ +/* +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 isMarkerTerminated(r rune) bool { + return r == 0x00 || unicode.IsSpace(r) +} + +func detectMarker(line, marker string) int { + i := strings.Index(line, marker) + if i < 0 { + return i + } + if i+len(marker) >= len(line) { + return -1 + } + off := i + len(marker) + ind := i + for !isMarkerTerminated(rune(line[off])) { + line = line[off:] + if marker == "endobj" { + j := strings.Index(line, "xref") + if j >= 0 { + r := rune(line[j+4]) + if isMarkerTerminated(r) { + return ind + } + } + } + i = strings.Index(line, marker) + if i < 0 { + return -1 + } + if i+len(marker) >= len(line) { + return -1 + } + off = i + len(marker) + ind += off + } + + return ind +} + +func detectMarkers(line string, endInd, streamInd *int) { + //fmt.Printf("buflen=%d\n%s", len(line), hex.Dump([]byte(line))) + if *endInd <= 0 { + *endInd = detectMarker(line, "endobj") + + } + if *streamInd <= 0 { + *streamInd = detectMarker(line, "stream") + } +} + +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 posFloor(pos1, pos2 int) int { + if pos1 < 0 { + return pos2 + } + if pos1 < pos2 { + return pos2 + } + if pos2 < 0 { + return pos1 + } + return pos2 +} + +func detectNonEscaped(line, s string) int { + var ind int + for { + i := strings.Index(line, s) + if i < 0 { + // did not find s + return -1 + } + if i == 0 { + // found s at pos 0 + return ind + } + if line[i-1] != 0x5c { + // found s at pos i + return ind + i + } + // found escaped s + if i == len(line)-1 { + // last is escaped s -> did not find s + return -1 + } + // moving on after escaped s + line = line[i+1:] + ind += i + 1 + } +} + +func applyOffBoth(endInd, streamInd, off int) (int, int, error) { + if endInd >= 0 { + endInd += off + } + if streamInd >= 0 { + streamInd += off + } + return endInd, streamInd, nil +} + +func applyOffEndIndFirst(endInd, streamInd, off, floor int) (int, int, error) { + endInd += off + if streamInd > 0 { + if streamInd > floor { + // stream after any ( or % to skip + streamInd = -1 + } else { + streamInd += off + } + } + return endInd, streamInd, nil +} + +func applyOffStreamIndFirst(endInd, streamInd, off, floor int) (int, int, error) { + streamInd += off + if endInd > 0 { + if endInd > floor { + // endobj after any ( or % to skip + endInd = -1 + } else { + endInd += off + } + } + return endInd, streamInd, nil +} + +func isComment(commentPos, strLitPos int) bool { + return commentPos > 0 && (strLitPos < 0 || commentPos < strLitPos) +} + +func DetectKeywords(line string) (endInd int, streamInd int, err error) { + // return endInd or streamInd which ever first encountered. + off, i := 0, 0 + for { + + strLitPos := detectNonEscaped(line, "(") + commentPos := detectNonEscaped(line, "%") + + detectMarkers(line, &endInd, &streamInd) + + if off == 0 && endInd < 0 && streamInd < 0 { + return -1, -1, nil + } + + if strLitPos < 0 && commentPos < 0 { + // neither ( nor % to skip + return applyOffBoth(endInd, streamInd, off) + } + + floor := posFloor(strLitPos, commentPos) + + if endInd > 0 { + if endInd < floor { + // endobj before any ( or % to skip + return applyOffEndIndFirst(endInd, streamInd, off, floor) + } + } + + if streamInd > 0 { + if streamInd < floor { + // stream before any ( or % to skip + return applyOffStreamIndFirst(endInd, streamInd, off, floor) + } + } + + // skip comment if % before any ( + if isComment(commentPos, strLitPos) { + line, i = positionToNextEOL(line[commentPos:]) + if line == "" { + return -1, -1, nil + } + off += commentPos + i + endInd, streamInd = 0, 0 + continue + } + + // Skip string literal. + line, i, err = positionAfterStringLiteral(line[strLitPos:]) + if err != nil { + return -1, -1, err + } + off += strLitPos + i + endInd, streamInd = 0, 0 + } +} diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig.go new file mode 100644 index 00000000..17efffb1 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig.go @@ -0,0 +1,153 @@ +//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 { + CreationDate string `yaml:"created"` + Version string `yaml:"version"` + 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"` + Offline bool `yaml:"offline"` + Timeout int `yaml:"timeout"` +} + +func loadedConfig(c configuration, configPath string) *Configuration { + var conf Configuration + conf.Path = configPath + + conf.CreationDate = c.CreationDate + conf.Version = c.Version + 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 + conf.Offline = c.Offline + conf.Timeout = c.Timeout + + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig_js.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig_js.go new file mode 100644 index 00000000..643e9bf0 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseConfig_js.go @@ -0,0 +1,321 @@ +/* +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 handleCreationDate(v string, c *Configuration) error { + c.CreationDate = v + return nil +} + +func handleVersion(v string, c *Configuration) error { + c.Version = v + return nil +} + +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 handleTimeout(v string, c *Configuration) error { + i, err := strconv.Atoi(v) + if err != nil { + return errors.Errorf("timeout is numeric > 0, got: %s", v) + } + c.Timeout = 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 boolean(k, v string) (bool, error) { + v = strings.ToLower(v) + if v != "true" && v != "false" { + return false, errors.Errorf("config key %s is boolean", k) + } + return v == "true", nil +} + +func parseKeysPart1(k, v string, c *Configuration) (bool, error) { + switch k { + + case "created": + return true, handleCreationDate(v, c) + + case "version": + return true, handleVersion(v, c) + + 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) (err error) { + switch k { + + case "encryptUsingAES": + err = handleConfEncryptUsingAES(k, v, c) + + case "encryptKeyLength": + err = handleConfEncryptKeyLength(v, c) + + case "permissions": + err = handleConfPermissions(v, c) + + case "unit", "units": + err = handleConfUnit(v, c) + + case "timestampFormat": + err = handleTimestampFormat(v, c) + + case "dateFormat": + err = handleDateFormat(v, c) + + case "optimize": + c.Optimize, err = boolean(k, v) + + case "optimizeResourceDicts": + c.OptimizeResourceDicts, err = boolean(k, v) + + case "optimizeDuplicateContentStreams": + c.OptimizeDuplicateContentStreams, err = boolean(k, v) + + case "createBookmarks": + c.CreateBookmarks, err = boolean(k, v) + + case "needAppearances": + c.NeedAppearances, err = boolean(k, v) + + case "offline": + c.Offline, err = boolean(k, v) + + case "timeout": + handleTimeout(v, c) + } + + return err +} + +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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseContent.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/parseContent.go new file mode 100644 index 00000000..c8addcf7 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/repair.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/repair.go new file mode 100644 index 00000000..44e4918f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resize.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resize.go new file mode 100644 index 00000000..6fdfeef7 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resource.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resource.go new file mode 100644 index 00000000..faea73cb --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resource.go @@ -0,0 +1,178 @@ +/* +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" + "sort" + "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 map[int]string + ImageDict *types.StreamDict +} + +// AddResourceName adds a resourceName to this imageObject's ResourceNames map. +func (io *ImageObject) AddResourceName(pageNr int, resourceName string) { + io.ResourceNames[pageNr] = resourceName +} + +// ResourceNamesString returns a string representation of the ResourceNames for this image. +func (io ImageObject) ResourceNamesString() string { + pageNrs := make([]int, 0, len(io.ResourceNames)) + for k := range io.ResourceNames { + pageNrs = append(pageNrs, k) + } + sort.Ints(pageNrs) + var sb strings.Builder + for i, pageNr := range pageNrs { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString(fmt.Sprintf("%d:%s", pageNr, io.ResourceNames[pageNr])) + } + var resNames []string + resNames = append(resNames, sb.String()) + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/Roboto-Regular.ttf b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce GIT binary patch literal 171272 zcmbTf2YeJ&+c!LCW_C9{yQ%b)g#>8<(iEkL(iKp;+(_>rRXU2)(0d5INC#mv0+N7` z(4_`Znuwx!+Yl_MK(Z&_|2ngi%%IQvyx*%oW_NZsGuOF#JtHwmlEQFMrPyXKH)*=B zv1h6zCpMQPxmUBcZQ2!=3%e%C&+L|@Zs(h|>(Kb;sdu|b@~m-^6uGEPyQI1+re<%K zWd9+!N{`+{dk$w~J6DqAkh{2O)81o7$5!9&SS!huQ}BKJe#83@9s8)qE=i87AxZ3T z|DL0UOMX%~?)L(|59&W;e7}U;z=!y*hQxjxGN8}UaUCY~n1Jh_mn2)60eyS+DH`~~o{Q^pn_lGslNB`KiN z(4OOlI~pkdIM2fGW8dmIv~OBm&#Qnh1M6KfeAuYb#Y3e&fTsd|KYV20;hXwB`9zZA ze*jD^Bpbeyl&Ut5q)2wjUkZ{!r4d??)8z_#&J*PHNCkAXi=)3d1{X*ksYz##oK!wr zuGFB5IFWE7g*E7+sj`H>)NJ~TLx1rTFWCV>549lulVI`Uk)7EgK@V%!iHmc^DK5rb zOo?NuEKaHFeL+5v#_#i77IaruIA^lgYx6wWY;$-g%VP^&@;L9C@|zj*o02STDE^a8 z6e|dlYX1vxfdDQLz-8NQo`C9|M`m2JiyS zxVp5rGNhw*CM=kJF6y(A&u)q_Tl4^|O zjwU$R2~k|Tj6{Bz?hPWJLgZ5OtE!2FwlEEQj0_&W1@ zebMXZzi)D+MMPRnFt|MO1riYx^dzr5`%{V3OoTx z*)FwoiHbA3jOo8;^x8TjxBrRQW6B5&tYS#%*NlUI9 z6^Yfl;}cS&#ZpsKQxbp%mXa1LzKJz|K?t%Xvgm=^rp?&0?Y*fx=X7q_tW(Ej9p&e@ zG5hvyyof$&-mz1QW?i^rpnQ6unl)|bu8d~Ww{4s2Xx+D0hZe2-^?SYO^0Xb>=Qf+R zW7}MNo4&m}v~1I-cl$pYt0Nmj>+~(Yr~Y9(AytwRrDSQ=zwko^;HQEi0%dSghL|J1 z0x^eH&A$S@DDlMv2QviQzZ!I>RX$`@K(j8Jvpt2(8h3RI;F zZXtw$I~*(xyy1v;C)$C$%C@@c%t6efL`0{jr2&UPRo}O_^lW@$d*+EQ!v?dLU*0yc z;C9cJr~7{nF}b#M0$&@qZDstJF)!Ec^UCWz*sOt`x5q4Mw)ODd8J(BE-iA+}y1sb( z<57*9eA8+;+fioX)G-T`?|Lk1SG!Y#m%0pt%=A~|B(KmvPF?%heA)-VlD_dqzQ>l^ z7V|~yEBtx}uO=_I50+feR-TX~k1EI?7M31umlcOIJSy6ekSNa>|Lc1ROAh(7`5oR) zm@)pI+~ym$fwgZsVK`6W-*({d@Vh&EjA*%swI``fz%|pKx+aT|Wk+IKSa@oREGMQ% zg@wuKZ~rzuan&KY^V(jC(;tkLn|#Il^Q|pLjA6gAq}Wf{Kvt>kh!%V&?}KYrkb5c} zpbJB!Se^1J=&K6JC@AZZlFE4e@#>#c1_E3q-3DDfzY|Q7+EzC++Ip>k0LZ* zWSq~IIe-Un{0ROI?-;HoV{M|v2L=xCRw>rkj4N=V=yrFynfFB+%v$?RYGx*ECx8U zU8S%?b!C?+J;hR-vO8W@Qk`MRb;H6Fg?u<%LgS>X0k`k{__ccbm!EGdInx&|m@3bn zJ9~yA59NpW37G%nzuAlIS$>6|3jX=p)!Y2~y*r=X#Y%Ppz#F!Y;HT8nA+rH8n*mv= z3nJM_&ef!Br4%AfR_If>8g7>@wMw#lBbU#W!z!wmXKz1^Yj=Z9PR6>Ur8fGtEk@Y5 zc6M~>6+Lo+rZ}VNR!(QOzv&An*()tF+Ns|#E|nP?0!F8@P$-PWsmVo6*`whd=8T%N zGKVQQ&fSB%GeQ1hhFtsI^&LhoSv&8ON1tb3!1reHJ>@arOOPh&@C9S-!N%Il z;-oAoMJZV5I4McHB(ZMVdWntDlIUG`iH-CGTX{a%3E_hA&rT=>UJFkk2hLO0>8VhF zqghvis>xW1ldOVUHzm-FWIff`%09~pO_=CrGv`jrEFQNo$9w$ZPZI}?n^r9Ge!Mt0 z^PQOs)-0W?)RkwBWYX*rAAEQ7Sa+>Pg6G{6|Gj-+)SP+K-p!DS3sXUjw&2+S=!KU0 zdMzG;P!hCW=C2a8EGb(qU4sYiDkJ|#^RoxyYb{v(iolO_3jt@zEY9UnW}ZEp>m4Z1 zJL!Foos42XuHbvv>qC{_ik_3tRwdb41!<@WI4VkgbSM}Q%?zvy5|At~XSP~;d(9Ed z2q;HVb~)3hHb*)76i9a!5G5i?zwU+|IoG9wP?(HOV>{y-=vOwQ@*;3k7~#te|?90uYNrFUC-(z*{N8A zEKSE6D%i562q{i_O<#pz=YOmMj9}76ScPB&tRm6`DFClfdJ2?Ay6vi?1Y{{S)hSg` z;^9u|-bG4+(kA|Tu@o`t^pS7>ym*-xu}-Yrr`HOhmclht@ zMQ<~6$c`k4%*6OK?e^9ZbF3A$H5o%(Kr$`jx?|%*p4_Y(La*Nmk7y9WZf*3 zRmvseD=jop5EzEVZ~hQb?35%0pS ztZG&C2qw&{1wW8`B-TVc;fi!hHg_ttMwPlSiH66mG^n?+$2*7m4LhH4{W16O4=N{q z_ZPq4ZEfXs<0mXzC{Jn9e$t(cB|lB#AM+p6cqcyIwnqK_$;kFC58OSpa(RAnu{0Bw zaj5z-^kLXv5_7=H{jH3W;2Dv56M|W;L6_Qf@XKdluRGwEiTKS|$|z=+oI#TDOJUB$ zFTrQcrQ^y3wI%p0%EV{*7OEb$8jG_@a)i%Z!e`_GnpVYsDq}$JGLZUq`f4l|Ef(~S1--M& zmpT%fRGv(hraFb>|GQO_bgKrFmY(kOdn4UxAU4c^11@oCEZtYG`|7mzZw>4>DM$YH zz{giM4Q-L{3$0g;ozs3+_n`yF4(Yn-^ttT4JBPfM#gCW9+JpG>_N%rPumg>yA(E%A z?zQ*ql%FnxxtMLg}5KGy6HYG-`@0@YHo?mp6TafVWzKr&?0B+w@_m zGrLjgDX}l~u5j@a($m6dRndwaDmXn%ii=lehdoTCvuF_n$l+mOFz0F*vq#aV>ERRw z*v|%C-+w4IZEnj2OTt6PA07DGl(s41OGnJJhw~h+eZtS|3k+Js5--28|Ai_IE)0Ca zvpx(8y3Hm%c+C3jDf~U;wazaLtITRW+vx3;?62SX58FGm`{BYCrYx)8(0ld!ulKPT zEbBDS`0Ej0nqR{`DzWYzwvCl%;q0RaU-LKkq6Jy~y8E-gE8ZurMApYv5xUa>TiRX= z2n-@z1vrr?(3zst-8S4pLNE!D9oV*$M(|T9*WmH9u(B2)J|z}b;6jp)Rg??fb+b>& zHMYw&PkLWv4<4~ed^(agZRK+E4#)-EXEg(`gh0Mxq|Q137K>{5Sz5FtOlWjcc4(>i zXnvu>-GZTVmVQds*Q9W3*GYE8=`ov#>)i~ea9ZN3&kKlF)U z?tf)_&0iMYB!ar9U@%3B#PQ@q(ruOV-nDtkOm988w>-@|nQj+?yG;O}+ybA(knSQ; z`d|3ue~zQHO1cfMl(g??rAw9MZu>$j#n4N`N^S6xUD1q;DqyDg%5ow+u(-Nvv|Elt z0OsQ*GBWC|mi%vz#_z3=A+S+*SyW738o!-?ntgqA$fi`jS9Ts9G;kYBIrlDW!3O*iSNqgYWy6wB<|CPTy zLR;uhQ3^iL*88)OW`c`wjnEC5b|u^V^1bnSdGhUiP`A6y<6R(+BQJu%zP$^0OD~newTug!(5fU6rlaPP`l3jWRO-~l4D}nii zmv8)@H+$;XrOf6je0=%d?K6|-WzOph?m-R{N-exA`yPk375iUrXgBEO7T;7P#nW z!Jz|}l`A>9=M{O!Ri3|n2Yc?~z)xA@T|4;E&t;~tNdEI*gA4f`7V0IBfounfNC2u> zZM1+05%$1i2=aLh0tp6sjNnTPRD{8PN`1rXnT#OV5om&LLc+l9GslT>Y+#;T_5lm! zfB(&Qur8}MZ(hjP$o0IiUk^X|?7Ov#XHQ+D0Is3M0X92u7%9aAE-q@WqokD z;IFt0xC~~}6hD#Pby>|XoW)qP>O>aPVRKYL=tBDQpSX<$YT3;3Or6FrG;dpiUk~t` zcj3tX%gSXon(%vtU+Q>%{KK#k9Pi}$pELXyO*nqSzxLsHJ8(=a8G?LMZ_QRlByDU? zPt^bFl^Hn)&8d53PK&M50)>Ehz&BBr^$C+jh_^csu`}HjN{o|_@}2qYo4=U<(rK*Y zMekcIap8`QS^TE_s`o>i=j*E(XX|=1gXEu4%NDkMmKG%2xai3C{; zfl;RN*eMHxV|GX>G+IJAVd)dBab-DCx+(W`v`nESrOckL*N_+()tZz9x#Qn=Sop2X zpWn;hzH-6(6>RW@-u&M8nH*~A`1@I#GUeILE@kb$Gy44a=_@7=>oT$5#LdI9KOc4G z-RUbQU40wtssoCw07V>zHLxtGL^We67S}*zjftsYURUrMM|n-PpDpakeOuT%!qz-s zYbUN6Ce_z$;SnX+vX~l6X3MZUW{i>C*d>P}UP^=^)blDXbtmJ+w~`<5yYi7e8{hxH z<|&H5$e%c6CV!)RU6inH@1Awa7k~i~fa(PdcIjD7a!}Ny>pY7?Xt7EqYEEKQVt|?# z4t}zXYTl>byF0z#T`pF$pHPAh*RL;0_Fu#refr*_AS}w%BBH`u5IzC)eJF%CROovp z0Jqfa`b)5Q!TO`q0YY>-s;X|5=)fVFeOuuf7Q2a+ts3)9K3~6=e-<6hKiSJS#?L zN0}su~qaJ!k+HB}N(ATk&>lPvq&9Ac5=2%v7C z8W+i)Q(i2*rBo_AX##ESOm-|dDwE` z(W8788*lsJ@whApS{|5G74?i~0lNbGM74LKkReYkA+A$DfO)UIQr^iWpO z5M|j4bb(0EsW;h8Q~?#qE#WR&C}Z7FcG62_NP3G*)xJPeThNT6hy|w6%idN@`dhLs<2jd2E6y-h6{}S zxNr1`-ZOJYog8!MGc|z+c3R_J%y*BDeSPNsxjPPZ=sBuSv)5L1KD3#KEbr``|3>rM zr#tncHIo*O1WShJtbK*HZNmTeG1EL+$CTDHxPD60ho2?7UM zJR2&1nMy-IJmv2b9Td2v#fG^={mbE^ERh;}H}Ar5|D8F**_=B$OJ;x6w!^*|%VgIh zer54wyW~ASmtW;x+s7Ao@)|oYg5v)H#qNP(6{S|1vr-IT&_!0H^9+y;f5*%_v4grvqebCV^vJZA-AEM+2y_fmzwT(IX)|b3+^o?Z)e)HSww{YP6 z)vKnBd!yU-J}i+*G3)-yxVdNGtaEwpLuU!g-2vyXz;Y;h9r|fy%2Qe1Q%1+KUB|LO ztO2s&;tMPr@M|`OGE`cCctPutrQ5@rdxo&5!0U|$j!~6I;zaLgNOvV53)lVL(Idlh zcKQ!Hb@-Q@teKwI+U?HBk`o@Yq^WYs6KQk?OL^otSg?-$wh|gwmbwA@KY-;(;CYDV zC-6)NG(0i^paHrO6lmrAM5eSH!t-*M${`>(#fctkno0}Te+$>s;+omwQ8N(~K(;(i z)O(O#L=C_Zhkg|K)m!}D#4q2w@{`xCemJLBM)HUZeq(r6m|V#(BZ9^K9>3AMkpINJ zuWmTmwsGgn%GvjqhRk10+6w(=@>zQ%R@7GujUtiM`9`cH)+gZ>iU{-k^csn^O=Tg< zvzk&w(4L;~0x%fmije5XNKvw+^AUgp?|@QY|!E z)GnZOOvx23QhZ7J%9J>v1zIXJI#;Fpf_(HeKx$J{iNVsz_tN>R$4~jEhWYP!{OQ|Y zKi^~Q?pZ5_hs1Ge*Nk1eL4+Wnl-1}6jt|-k1nrg_g8-k z+RTFj{|d6=l3Hp3Vc~){PF+TX@io?Hc!NSlLZF&MXpMSGfb3X+S);PFHO^%66LX&Rov8_{3B}FBiZ-*dUtvTJi7dr^Kc- zcriRZH4UwOX==T~7W_=uGQXGFsfSohjfOg8jBud_0WCq&+q$p_3up+7MF$v8k|0fK zw#H9nV++ zMcv1;KKupm9B|PZP_6@@wdTDHXbn>7RAC?n(VIzg;jfPq_GFx1(kx3AS29@A zSgKfe1XwZxEGbUPpehhSd@L>gx@0yHeco#I~%S zZS2`^Ur+mB4C`ah88o?nMquvu2VTli>y4YYr*#iL%UfMOJ9^Z_0p7au$$P7dep2hv zx`a$&T`sA;7U+{Ha$+p&vMj|g?E-pJ1R}yyXoWzFbOC0oc(Ld0lg^mLORbl&#w=a{ zOP0FA_ecR$q3EY+q6Jl`NW2dA4fpZ7U@!x>hDo#-J@`?k$^jWYGS?Dy@j)j^MjM~N zV%N!EE&P(X#@|_Ti$BQSHgEpc9rI>ymlN0XIs76IUROAe)h+Vhck|2B+Lt#0-8|ky ztt6l;Ck@xab(t@}tIcrvkr)k&)K zP5#LdIXnDEd~*EToHZHS+qc^_W3BA^n}0gl`?u}pxOx00f1-Z$*>Z2;vMZ#;y7L~& z1K&!9KIMJ4p7=3m?Tjs* zI&Ye}PEIIh&%8JENrJ47V|9xBaz9%lhb{bE=U{mS)(|W;)6@{EYU)Zzh@63Vi)5DA z2N*h21B~V$s5d(?m;zx5guuxws?|C0V$MFL>$bEC$}-1lucX$Syf}oUhrG8#xHsi> z{7rcs3(<{6Oid9TmDG|OEIR4T0uKC`QX{Kawz(q*zRO}-wR@rc=(aNs9$EIvTb-M| z^m2oa<2x>+&zYCwH_pf)~_N3o&^?BA_;KDw-dR6C=Y$u8rvDqX^N| zNk+XprXm#F2WsdEBejN@)h+Tf>5*WCgjDi~Tx{0avuQ8JKHH+nq<2o9v^C;S7J7TW z_+n6IMCKfM{X+C3FewArzXg5agziHAqlEGnMm4$`gu1er9}x-&&mdR?=}&tGl-NuV zxg&C4;HW88hg|+(Kg&7uS<@WP;CDYcDd%~c{IyOAFFXZk;$Tv80nNW=j0`jh)-z5@ z6o4d}QcE&M==co!m`|F|$9-I=G%P%&YwGH#NngR+AgPCD6aI$I=N6h+_}n4^#?1sC z3>~gXfg(J!=`R7|1#pOr5rx6w;mK;tf*gJ_lRqw&GWn^4pBF7JR-P|BrKA*{SL+pB zRjOg_&tUCm0b8KaHDLitW3BS+)N{|KGOp`)L z1z7qL(dHjaYziP`cVc2{H1#Y1ko!fa_^W+yxtr8|b71^4{GGEbRHVnqZ3P%o(|?$o-esc2P+w!6@tf(G}n zXn9=rho5~W@BJ|0^0sZtMZTo&cZW~^vH16bkM1OodWrw{?6+Os`0gVAr79=f zja<_|dS@v~#a>>%od0e=LhyzI-jP0dZ9aIB9x0QgxdMfc>q^pMT!1&s1g|ZO$cjeX zG_+8s17;^8jwqDelOyBF#yi5#Iri_roRF=t&pz9~x9~+4aO?Z_um5zTCF%nu9yNuRHJ7L<=yD}on=<636?j5LHXy>%8;cL0)@XsmCsgFD zg%p83(jlDbsAzCZs`}v2?B-K;w5-;;{l;8cIsP=4#ys;+C-`cLcO_vKoqp1%KC_TWjYCi5ap%7H%L z*}AH~!2_-)y{O66YtSkXKqmTpU_*D%d=H{vSTA$p5Sgn)3pv1*iH<~wN=kZSx^QgL zqaoTD&Tz1ZsHnQ4**XDiYggN>zkF%^%&Bt+3|~5R>AK@5)-RvF;;nuQrx*1Yb>f>7 zBSxGYKH|iv;nSyP%$mRE?8Wz2WMqyTpEY~c`{z!qUz8zFocP&u{9s3NL4n23T(+5v5)?Pk;t=x;g&iO>j)-XT;1;Zne{ zKxzeENF)g(^fYqp^gldi&eG#M4@&Peyt?!3sqtixtkVHD~&z~NZKa_ zI0NuOh?suNc9|HMLZi}Ct-Pq-dD5KOv89t~o?4LS(o>(AAzMxP8iQ26?(r%SVHhn4 zL(^GhH??1)G9Qbk2VWP2+WmudYd=1^dc*D|-MhXyWXPNU>E^}wQaEeG!ZxhqziiyV z@2wwoh_zxX%#zSDQ}FSc%FRd(-W z@!PiWTRYFPH%_1CThE^4+b>^YaD5;-@`p;Oz-JX{m=*$m8t4e2#(iB27;WR4njl&x zP~?&dG+Ct+El|8ru>}3#Atv+h3e#-+?kOAhKkb>U2Y zqUUQ9tCnBD8YaIfp-;M>v_k2ld?+QbB~Q5IKqLOk#T;7iT{c!ZqQs&vsJy7$G3X@B zlUV{zKq!=wawOL~QEAA=GQW*bmeL#G!S8^x5b$>jD#agYX$^$@r-{Y@9HHTGbo+29 zTzi8T4NaOIUdk?%tSw)9s>KRF-xHgp#p|7N@!-#RXFT{bH8!3ogbB!_spvJ6Qk|(t z(8rkgvuaE#{UX-sNhV=q&7(4rWZQTgr_#QpBR|ncTJLNOwX?VnSjf zjmRJw<9_35#v29J+^~^FtX<3R3D#tJ^I62o9aAPS*WwVxIm)x9dFR%B=Eygm;=a?w zojX|k?p-X7xbsiGM|o}9}ho3G+rLJExD1|?bS6lf4;#ghVbnYGozY4SFcr@AuVy|o`@>` zRR6%8L(zXPX7k{=mBir4Fu-a3$E+U3;O3SRTL^iK`vPs{ZKCX1VkP0AW2y3NHiR$R z#@}V{ZDTJMeXP8sbX>uEv2`oh+QMKIVVTreUM=sk9m4uMYJMi$E`lqABSrQw3c2X0 z(&eM#swp8+#7H4yqgbC6-E!LEwu|OyW!2qEq zl@)n>De1s4>0N1|q;%67Vi@c|C_2!R=u8ZR0b)lf#9BazK0StsFq4c$h>0+*qJBk; zgvFNr3D!l`k&r! z8?MVfT8!L{TuB8La77G>QisS3U-O5{?GnF9lwF0);C;lcVbW__@Y6jwMsy&;cjH8) zQ;dwD!HVX=4K2_StP|B073E8*Pz5p(8iBQA{YRf}kh{&l+s>u0A!+TM_5PYHCARR? zs97y|b(?_cC2)NscwqrjbxjsM`MM1eNe>IRiF~?5ei8EcE;Kz+J5-!Yp4tAt{BIWU zUluI;@vEN~KWR&AT`CV|rTmhfBL=4=)u@JE8r*k+yqY}#KJ17On5joeggzF05O1rc z1D>UvSTf)VOXR?SCws8=I_n(_Fwy6Z4J?FNWk0O$(qIWzcPSer(dS5B+7{bBkP_Lc;xFVZye?y9 zBtPbmW96%$kW(j&fgmyI1QxJ;BK~HXbPrx7{q0pbi#gayrBdnN82x7AZ-(J-et}k@ z>#v}{kz%m^cy%0XU0wqvTiEGd&Il z%A<$&nVciv`RK&e4MQ9ICXD3mgWesJ-@D4tQ6su;JpajuAM72`dI_r!=eTAK6d~o} z-+}B-;J8re1>Z!i5d;Y)w{X0X>C@1LN38C8YTVS4K0yzrC?KH~_Ni_Fv&9PYb%({p zDKtO>gGIBS;c*zFadax0AL>1S;TvVR@{#w|2)VWQV3eHyWG_1t!+P@dHcu9RW`*|` zHctK$veQMnsC=pRh6R0A>jY;KEPk3L{&v>l)ywrlA=mmf#y)D&5jcIl<5g) zEFC?n44lHcV6Oh)SPTHP7|Rc`mSg#Tz8`08S(}MGTO7D&B72SRg$hGo^ZS@Cx`&KY zEHbA9G__iFx~xiGF&z_pvSk@PE5T+tr%08$#S4Xz!`NTYf(`mqjkvumMw5{ELCd-Z}O?KaIC9d2g>6H*p06cg#ioagaR> zG2mb=PGnw8io+-s8^fO#&esCM$$8X5Y}B9N!5FA{nmJbg(yf1qq*GOMSRRLBuFofo zjHo2*-T>t_g|k4xx$ZN#*vmPWa`&B_(&})>a|d4ApHRKdtkl6HT7KMV?tsoW)lLOJ zf4F|~xhBco7iGM%UaL`Ib!?3{Ur*_=bk1vCF13GiF#iHP*t+JZR}`s|bBo(XjUxSue@9$rVY~wIG}5W(z`#Ptc_xcpK;*ah9%C z3l9}bb??4CZ;;Y<)N+?xZf3;3j&FXjV(p#|gD1ZATKK~K z?b@~J#EX9%sZ=$q;LgP7oPDa8z`9w1RDY zC?wWxg_834?dvmV-5Njq(tGcZuRZ@}i@fO{J@;Vm$1MiPadZE(c+<8ilULdz`6J%H z+dA)}_r9LF^v9_qkI!G$ds^2z>(l1G*Qe-@XY02(x^3QxZw``z&Jhlc6in!S31FjoodnTaI;GpPVOF+k$Dk22Z!BDC=x_#8J z^cxsG59ZEHEzV6^8RnRB;n2LMT)0-YyqLAc<`A)DHbf_aP`wz4BL9~(a=5O9?LHlx zmfCgVorQ0`*=!xUwB_hlNJJ#?bcbOO*7k3GoWh_Re!NjN)NKE6 zJqj)oIZGpFqUce`8FB1iS`-``yl1EXqelUfvK#P6!*7}@p*$hn)VjO^I#2{BjN#4KcbY)ysuRqK`6!x+LX^$yjIj^H=LEQ(l%Ru`cUa7Vx_MhNyIA5wS%rjAt)iZn zRNz2yOVr_g+kC)iQUPAf4pJJxCeNIi?{(+nD1Vt>)Jy&nO_d846iGBrs7ec1Jhlpm z_bxaFyGbpS9S}8Id#j$d7zlEx2G}8&%H{a0RqzWM;%$(zubD;MEG6xdq6bx~3>Sek zhaH4V($?FNvpQV&*07j&S_Mk0Iuu8pW?C9X!+^%f?SNsL`!;kC& zPgd*kGY-qA>Iz>dki$Rf+S~?37T!b_q=m4+8)LKGxzz~dSyA$## zQDVZJBvMRBS_eQguqu%@F(T_oMZW=dd~)!|G$RPiE3Mj3ZtuOcR$g3fay5AqVGU5p z0g#J8sg(|usMQ%Jqr_cgy3hJQLIrIsU;rdyC%*JZYJJfm7_x3%?xIDxO5geu>wBZ* z_tPh)%iqwwH}O{LZ-Ps^YIZT}rh{Pd;Qr~p8d^mpU%G~EO@u)hOG)IXr>M-%*5Q#rmUg(huz&o6FF^IOl%~sR(!6lq zh<-zdd~i%Y9+}BPcd^vn%(Wr{1LteKMdd@(1)<+v;-|0t3=Pt=_#Wn0TO&rQfh_n+ z&7LYsDRzpFAWWXxK8#qDg{9TRXm1u~LHMquI2{4P^{gXaJav-H(F3`urqN3+LjP?! z{kN5cWv^yZqcuzHd6e)jk=YQ<=x{Enw)W3f!z!XfJtd*%_%9aB{-pkkCx&18$y?X4 zdFJ79Eoc8Z>q5f@r)S>ck8(E5oxNe>oLQUHn!PilBIZ<9>HpJ|BtR;yLjNn)vNE%DK|M2e7^x&VJiC9gQR3lF?9(EEE&q7gjKBaN8RY; zBa2S-NY@7D+4Ow-=&H#dse5)DiChr)Wnm9+D0=>FVS+hI z&8FuuY)P;7ew3142X=ODLF=`x5T;%X?dA{=S z;g?(H)=!FB_XDfN`mEtUZQF4>wCK{62!!QL-gYpVRj5~PVQ^7Z7fvL-!bUX^T zp=iFg#Dl^NmFSGLR51%sLIFo)vfSg`_Eq)khE#g##b $$K|G-##kgg9gbWYa2)&N(!P+kwf1!Ak1A3J6xBq%4W4Ygk3hn2GE7&Akq8YI z-YYx-G>F6FF;RhZw58EsPa~8}{8BkM*=fVhh~}AUm->iis(10fmZKyVxck@DJ-Th< zk9)Esmp&GQ)kn|ibJg2fgG+rrWiRet?U**5e^`I_Un=MoWeiuBV~nCD>IcqMsfWOg zRfX$X5$>9y6)ifzh|4v*Dq?Wx3RGjPkvOf&6l9ioHN5l3&Vb)+qB* z3;5)>`ENPf=Fi=>V=g>$a>VEO^_jxIOrEnN3eGr7E=0%h7dg)TH%0Cm(^U3~b{Y2Q zRV~P5kHQdAhZ*z`6TrrakwVv4u-G9BMgR^2h+|UKV4z3>8N~yaUH-?c>!_aVvyZWd zS6Z0nT|W<;z4X(|LEd*x^P(u=+C26O{ehlJTd2ASlO;VhhnV@&<>8;ro`yUa9;wi> zC3%2IKY{y5Dl(vfUz}Kb+5tO(Eu3jnn`LAJIn@@rbc07NZMJ;*<%;T}eM{A%L*}l_ zX|lWd5R&12n2hKP>ltk9!5|cm0iWOvh^Sfd;NGRS8gj?_?#y~Vg~Y5mrW}Uu)O5)b zk$Nw5nf|D@!A@`$kgM~nSc&u%TpK%*qKGf* z-TOjW48yf0Rcvzr_VuG3xYCm&u_!?$x9zL0p%&VM~y?cB01<=|%yuuZCc_ zvDvoLx=SPfP-l!Y$=T4UVq7MUw%|pqDtr{A$O$If9D&Lj7X=kk-S35WJv41NaY}@juVJ(6f4lXX;HF-_8AOkK~x@&)IGbnHkX_xM3Z~;CT`C!d|Wk zEAXaTpws}5(Oz-b4}_W_5xV?KL6hvQtpKcC5*ZSp4sf-@sCHsYT({iq68~ez(33Ya zZN>aDOX8Qw*1W?9v(Jn7i>f~4L`iBCC@D@QR;jHtQf%EQWb;pI~K7M*5+RCwfRqI!odSj)nQtQM) z{X5ie8`w;eixSnl#SYtjLCy51SF2OcPC@;FP(-mqc);;8zL%Ut%Yec{Ed>-3S1+TD+_o;@1$DW+c;l&S8UVaAvuXbrfL+f zMo&PftzS==!l$oi&U$F@fOJklhe?$uJ?%uLBMv3i}_1$aG^>7JY4_YxDl5p5}RK6t3Bh2|A&;Pc? z4JE*QjdLYi+*n=RbS2MQDBD=Qh5S)=$tE{@ncrer-$m&1A*z!t&6@f-Ken@EkDKlM z9jF*^Tpu`ECl=xbb*hL70qKOUcScS(3T$ICh%i)*Q z*@f8Ri@F>X;srHM(8~ec_PS0nfwO;5%tU@-S|N;Dk_~3owC4k&&LaqP3f=szHQ#MWH4+T@&SiZMz zp4!IXN+vbIDrxp0NNVseD>Tv~78bzrtV@BeBV=M3sn{(PFHHWOzodi~F?NT?C>Onz z*&+ENvT+OLmU6R2>%8c5R%pLn+i2W55`LmvdP@t?c@~}WWs%-1aDwLt30>kqdC}t7QW01(G(_ZSxNk_Zvs42j| zPD@i7Z)9xI!s5-x3i+AIqvw8f%zO5jwl7cFk+1DLs{XCad9r5RliBLty(&xkb=mzE zn1S}jA3TFfxO#T~{OAolUWkcTT-iCVKK|J`5K=YP*1D0ytl@_ack`r1x8 z*!%1HKbMB`Og1Q*Rr^IQ<9+b{wX(`)z&rwcaSj@#GIADW#k{=E9-_`>Kvt5Mq}8|) znTh91SW{@^z`^Z6Lzh_=kV%g#K#+~usWePFq$I@Bhy(V3L~S5Jj6YCC82ylGf2 zwvJrG@9vwrfnVsimh^9*;-A&A$d5&dIfxiB2SLLM;qW>MeoMp_g~db}5s{%N#m|h{ zP2w}tydLV<)IOy}iWkZOn(ElZfu>;tupe#GAsk9yX@oYg$L>R=H4){$+&Vlox^~N@ z34<@^-Tmgoxxp^)`6aVHc)i2+naeRq_U$~|?D#EPSow#c%#YRIINzJQ_joQla`;=U zbpxNGz6$EWzs5cjl0FMTIj2zY4%TWhJjRN&s*>2ZwQ7>3fNZZ)l@=BfM3xBNggNk{ zby^puyE6KosG?I1)jK>B1^yg1Cc&abZvpBhb<^Z-`9JsSJaO9N3;W0APPoMSXAB;a z$!aWmbLOgfLo+*!d&hR-i#=VlYSlbG^}>VhJk^#xqqD~#h8ncDH6KU$bglMti!Q4jd5z_BSd<D1>-=LtdV$#if@aH2(dY;o*bpYAXK8m^)fURRlNPnb9?8`lvhmZ*q0r; zWE=Cv;@kZ3;YFXU6*U4bL}kFk~hF<3!@hKW4DR--EX>KesJ$ zp0~H>+}TqZUzEK-xa^JS{T{lmsz@U>MP$Qt=@9unLm))V1TAb908-iTKXHtQU?*uw z@$e#!;$SKJhPtU;S}PkVx~7rcduroB!68V`P+O-yT0wfi=+}=(M$OI6DlHu|Vs%dO zsq>F6bnf;2+1$rD3kMIM_3*^kKe5`c_Im5J)j8Qqa~oHl&|=xv4;M7;+qLC}W$^Tw zG?c%m9ETo`K~Bj}r|ps;k51eN1_)0}=Uz5e%W&Ez33^-4D;=>?zHx)9csSZx=hWL?@eWmGTBR6fP69UDXKGJm^}+Jb(adBGpJ%otO#~D zsxu-VOIDLP1^a<1O-*CqeqT8T{WQ9yLK2=09Czl(9+op?%73QDqX3h!=H&Up&FX6z zlRC97dH`ut#16ES*{1%aO44#o5&2*W>(FnHV|kxu73^Zz48x_+LiD+f5X_l{kk^UB zzJ(#{L*xuX(G$2_?{4g zZLY)$BW;uyipB27VfViJ;=X$CtJ^=T-Z;6++>Dv?RDdn&GUNJ$lmpLd#P&!R2C;(i_!I zWKCN&c(0uFy5=-8pt|}tJOZK1h2uazE@C7zcN*Pa zf*MfUrZP8xK=qA5AL~htghU0dFg3VP*38yxTpZgKQPZ7ZuUzfb)(tBDmw$7S&FK-H zS~H1Nv)ymoy>M4@qLLL&+t1I|k{1L4=DvKavI87Z6a8vRtt3c?b--s#gQr?sZ*n(MK?I=9jPg` zRPyC~BU#bP$mu=jZ(y&^$UJa*5euCZ+h#!X!Ozus<-a?|zPLGa%rqw7T|C_8SGj+O zFS6t{?+;)5VwH$G0~>9t-@efc4H9c5Hy*fh*y3}ws%7<9pOZ*5d8YWGx*7D2fL9bK z@>c)iI~dwgP{(L~As4_LCV-30+ruG9ho6L;h%w~voAB4UgnV~AD@`4-ChbL?Tllb? z9cpuBqjzMZ7X{DAvx>Sa8&|?kEk^%J4E!A03#5w{rtbxUeaMV`Z!BuU$bJb}OWLOV zMSj9u*?Y?F69a`sM~m&p02^$);ib;Sa(vHc4GLRy2s zGV#2pyu~RNY;M?&NT9XH_CnL@)x%R5yHYKyaJJ7Ym`g?n;jn{viPmEUOdw-7^!uoOhG|HRi@V zxlz&j4RhwM-#B;ndS&A>{=hZw=M0uq9Gvxg2J>F~=-Q2QyDy#xo?nA~zX5oiz_WP| z2*Ia@B{38ijcl9Y#Dt8wCBtA^0@YQLAx)$XALRz0Kd^e8YOf5M{5IoTlniCFci@b2 zQ&W~Mk(W~4bs^yR3vke$r6DGZW+Aq~mjRR!Y?z%6+}Y(Mr!qlFj&eCADk8gBi%;I$ zX&ZBV1TVgM?2L@ri1GZ=lLOzgxZn7X{4vk}`kIXAUdd7?9&dXEFq8$?y{U!j9p*^A zmV@0YqiZb@Ya0+)Xjxh;FQ6*8+1rOZ2Li{I*1b`gt&AWu4B8gG=FxiBDwGx`4BX*x z7N}kkDG$N(i++CZ-M$+G_HUgtV(Oi#{5CJl$=P|rwqUgHsRP9l$rm^DdvT_^43i-Y^}?Dr~San z-vj!+ydaW4$37{?(lA2#UmkMoZdnD1HnE?*y(}PiOI@|{A{U_RRtra1AT^#xC017n z_N5Z}q$ahh`Aeeu6jGp-52v9c@Qdv0_7@PBvJP#eNFKKAa;CEra~vZF4HjpLzwk@<-yYAf?FEbZ z8N4+f(ZYD!DfpUIF~=RD?|_MDA;ISpS>ouDmZ*wlMN3pgOXt59sDFy2j_ENKlxTvR zg(q;jMRc8DW;ce!2CW90!=(GR@=Z;kGzU4;E>tjx3yJB}@h`NKzdLj8@7#HlMo+!L z|2UYrUTfIgyKK`O+tOw7sA&JKSML9PjWv_GXW{B4SzBXRxf>e}oz(b7TR3}t#>bV| zfKw+>Q$*2Leam(j-U{{*F}xKpoh8R%No$nUYbBMM3Q;^WR~+>gI|zrby}}{FGk^>a zg<6N?%6F;{?$kV`a&ThP%KX|5%#-M(qYvxbsI-&0lY}N=7=EKUOuDI;a$JAYxnEU zj~k7)UFKYES#+qC+N1Np5%M8<#GsBnl#RPj@(29Fg9ofOi#V#S^!~N;^qC#!zTLKo z`rtNg`vx*qt@uw{w#Yp7K?k(Hw+X2N3n5ChJ=BH4~5~ZA_+(wpI zEL)fX5Jz>YZW_lTtwxC`m;-g0_pi0nAF?oB^ozVYOMZV-=A3-qQypXr3u?tlV>N$0L*-BT zP?IJ!R$t;v5MD|HJiS^@r7$baV316WTF~U??cO|a56czkPKPTligwO-ph-=UWjhMk z=?&!caGDYkZWC%f18*}s=eNCAxKu8*%kD)Dt9I*?5?DzY%ev3ov~~`j_i#@6_1vjH zR9l$$D2&*x+45T1G5DuHv1l7NPe2XqSjEc&alrrhBTeF-Att((570 zEym(YylPs!VX&}crD*I$1x^(YE~dIV&|sQ&A=Wi-7Kikesjp8kF#32)5CG;yTF4lP z87W`PV}rh7*b)6J?-g&|{Cs5r>%L$^)*R(IH0!NXzWMm*kE*=J+Xon6-ai*i7(GuR z%hmw--_NklNU6JJcQ8w4`(NovX(5Y9Mo(dVk%p z_sQ={KAXLH$JRVP0NcTY@wQcOe#vXHi&-!Ql?i!J;~TfUO@CtlbD`$3wDDuM9Dumr3V zzL?_UeT(FmXe@-hX7tE`LStc0!kik1_A;@6KvF4*OUSFnc^k7qcbSB3ti7Kq@8Ycc z!u2cX15vLicTb%BNd;A%-Y>JJOk5ziw=Q3WIY@fCLL~C~VLHSRB-&G!81HFyWvUla zRSx^lm=of9^rssUtjblYUjZ;M=R(yMnR09!o*YM_X_sxMj~4!%$Hu-nEF@Oxf28n` ze2Rxh1`p<|Szo?NRtB-YpUcCZ{Gz_i+ZS@us^u|_m@4>imab3)9u3l3I^8VQh!V(Y zO%|e;q&eY?!1_6n_H#n5Uc$jl7({BCpD~*W1fi|g_k1_%Dt9CIfb=W1(Ch8x>h?8x zTX>&e*-!k1Z`rghdF$A;Z_sM~(*rrnJAnS-NgW<<2ASBVk@mMSQORv}|ig^e(8D5$yg5>=EKcR>NBl7&io zW(iaGWCKxLa)>98#3LEuYp8KnLE;;s^0FN(r+kq2?%g>L(Uvp}`sCKb#lK9?J1}qU zA%1+{hf6ZmXGYJSKVbC1{$CvLyR~-5;uHVve`MOIl}pAk$+k{;hgJ*SDJ0e3=&`qm zsfgqa!dl2zQUuj^+Hd4PM_r}vM6)3JGW^Bn`;Gi(_%HY0;=doQ8sI(bXS);!_P_?o^B>*unBh038Qj-0^STdB485Awl;p12EDQ#zt9ii$r z#PhA>wXDroUT?}9j#LdVreo@R1whr5S@f4`U)nDgbFFZ7Mns5;$hNI5J*3p_Gl%q%(UAj zd=wPfa2=vql)in9!;Qzy)6-_0c=B5^cH7eD*My`AYwlLY#cW~D6XwPrzC7y0V%FI@ai>0h++ z=d)*iBAo9(m0=$gUh^4@->_K> zHkH7mF(ma2?iGezO#jDco`_o*^fg$J8dHRENir*7U=7?RVkX+clDzPAmwG0D;O@5` z3&Z*Tk(0!2a@R`H*S}&{c-^d-X?VgmgOZW#fKIoWq#-R!7U^yS1dCW2QecU*QClYz zz$yU<{T~~_{yfTl&wG5shXP?YaYMQ{-{=^Gx1pHir|y~mAGbHcuYca2I<_$EBwOGR z)NmQFEDhW87*Bf!u1M&wu-68}^dzci?70-CPhP^3+p*zlY2T{FSPDC0AuAabTA*PPQHdlq0&cJ@ z2t%jV(o2aeg3ZtjPm|ovktNQ^l|0=OQ99-2DR2J67xB^-AHe+kJ`YNNqE@h&GV%md z2hhJ5`%-21{|;sEe?s_A=!i>~!ZJS;(cBU($P~?qWR8JEW7l757QjBfCcB{dRnXg^ zllDFEEB2#M&W{2(KZZ(?-zEBBJ>F}@zE2U>`B7MhgIU5U18+h-P7yUi)JW7Z9+4WC z0>zGaCrzS>$+c9#F{9R;gt#J(Ty~u<7A#nt zFrh9DzB1IM78)SZC75aM9tt!R0H<-EKCWf2Sb_b0(^VAW1jozvUrB1`|*$HB?i zT|!b3H3_ZJTXX7K!|8+?(s5)_EokdQG&zvzd8 zws5n`F0soECByJRyj^)8Pl>*AfxXWE%Upl3OGUqIS<1@s2Wz&->E2J|A;Vaug72TS zRUS{@z~lzYKwHdQsa5SRc-B?1qjd#6%ZI(F{d{nX2-fHcFd{rn3KLNr;?!t~dXcrz zFnzjT1n-vI11baV(&d0#X&~P>Yk`_(Iufxo%-}%PIV~F29rZBEY2sOB{m=h7=F3H> z4tW2b*DGtpq&Zu*lwGuHHLVhW~WPiBAS3B&No>k`vajEmv=M z?VLXD`g@{))f_$Q7x4+?!rxxCUe1zM6XK#~^>}kFyDsuOwb{6g)v$wCtpqgf1Mc$wt(X?vN?e7rB%!uH_*&zTY_H z7UKzG$SHM1$YrN=b-~oa04z2liq?dYaY!mNf(p_`bt-{DMVgd8V?mUx*EC`j6~WQI-C94PHVtVsJXJ z6W+hPmq;FUF8W#71&m7)c^6GJ&gAMFGn9ZA;xgbJW?__Af6x99#}TC*kW{Ynn0ku0W0!bhDvjfM+A9L zg$!lRBMN^)#7Ei#^ox3)`gr{pEwpY4?pc0OTI{s+@G5}#eR=9KH(k5vfNVqBIz+!I zZ9T&QS#Re1k560w{Pq0&32f*Wq5~`V$&|K!&-)>*P<4z~!3u?tC2OjKF4>u}Hij7? zm(XD8q_&!f;U*ZwlbQ_ARZYhki-m5fmtVo;9Cas>T4wzYJ9fKj^Mw~aKyRQ`}<-u8%%)5I#z zZ+|IGv7KRsKP#0AscV2vLFnZGOJ|o_nT;OJA?VD!Z(ZOX>epOEt(x%!wwvyy&_^-?vjQO|9*1p~=8|gjh!9jO`rX4m z6g3g6tHwSt)4ml$ZH)Z!gZ)n!YL$6JFmn72E{|S;kQr1z-w8B&@dg;*?k8=0p~+bZ z8Q_-9`@pVc#M~|3^oZ3IDKG4r8|4mH3Z;8Hv- zk=U2sdM;7In`CBt-^&L(Hu5xk_0Z)@2YWa5H0gNYlKSHONexq;cy98X!eQAH>!nnE zX5tJU$U<1{3hbh&mLSfH8^4P0;zUQbtpbbwE9Ctz!wY{8J)s>qQd$WO{Y>trIId<( zI)tCWSCYkYMlF2CFH=oa1GeIKOiRmlbw2yU^IGXGg_ps+dMKM99V@fOpm{i_m7{8X zn~S=on0jVdxR(VyVNjD+Dnu$jC~Im0O-L+0F?Q)?ASXkrax{P9vZ7wio>hi)E$%^Uwb$|3cv7+zPGqA>@|c@&K$x-vDD1@bl25 zV!)e^~eg2~L|M>cFc zrJ#C)7&N3u*HK*8o9$T`ZwYA5rWJtdlVTk6I-^|KSyV4*ay70f`}?W$qfY;3R<*D9h3} z(pGcoJ_T?d!<%Cny)oebch840#$wM$@%SDYS@EbCko4-~%!euEmAWjN-!UJH_b6Y}R6$TI0@GAPHw@%)HT z;ugix(NLKrgZ$6qwu>leyKkLskB#IkvK7fE3R}$95YHdzX`^Dk%1ARw5zyZZvxWI# zsd2*M$!%f-Bv8WClS!yiI!&Aptp>tVBUs@1k6Ca@hGGDh#NRIA^>|qRRu;W^+kBJ+ z$hZpJ!dKtcqhbJ|p9vV9-Yfe)yLl$>CiM_QltDQQN+hgtA1z$3g$O*2L5UWwWYN+5 z`fF+3?_1i^l|*wYl1oE@GX;hXjV*!6T5xp~zl-2(zWAZ&J?0viaWnts&@L=V{5@bI z9UQh94fIYXtsA^-KNI`f_GWBAKj31%M_nE2!C2Aux)$WaHO4;1^0Q!>q{xEImVGz0 z6g1z^Lg*4xmKw9VV^WYjV`l3bFDvU&K#-fc0yfDED})zThZRD8$AuaB{O}eOqrPEc zpf*=L?<2mFJso%CK2tj+A-nmYOzQ0T)>|GHqk4ouf5*ZbzNCL8Zv04U^Qd}q_n=xQ zBqmPhKk{_0vz$%~dd^UsEk{$4M^h`OAG&f#$1N9Ij*r}TyxhfdM6c!cl*0+VSnd{X zw4!pfD&>f5%Qd8zGsLB&`4z zcFY2`M&j&F(E|0e3nWLB&o5-5R%0zBqc@NZNEMc>=if*UU@}dXKOs5Hyb0XSbWVwm zby1+>Is68QUm_^CSj>TsJ~}9>h&@r^t2Z~U-LZ906hFek`i&Xc2g0qdvVYmqBZ{&? zbQM2+!(pCm{fMJ*tXMRt(@VW%wGr5Pj;)w2;W(x~2FD>aV1rXxJO_qYdt~aNG6IIF zLr2E^%n<<4wCS9>5h{NifWs$PbS`9wZ}1@41r_&Qx^@%-9A*OFq0f?UU@Jr)HE(&& zVP3)uRH|uqtjG(_LnpYbZIV{Xhp40!Jn&Wv*EPhSe8rGH+HpVDD1`2nb;)7}Wi5Tl z(Y`?nbi0M(1Jd4*)t*P;;G( zmVSVe)Rp~l+OtKcGq|Umm7!Oed5N-6P_|6@{TjVOSnLYD=lR%}FK;SHT$L&Yn6k%c76O9fLpPb8vKYTCM66vOo0 zag6!$-C;YQs-=xMdWD@FnLaZ6?Bqd%(X9UA%`-P%>C)wu>+h;b4?mwYcg|!r;o;{~ zX3v?TR;1N?4ZS9IV?HsMg~L?;$a12k%ryw!k~w9Wg>mpTM}(Y(`B+E$XF*Uw-CB^Y zU1JV)LhZZHzkQ}l&kwhrU>?@3eR@WIzwTZ7!NucEcAr_c>|Hm%;Z5j2X8Ztt&6_-6 z{FweIP+JJzGaO_FV{7Gv_~N!m3iD+&1EB;$cm+)Pq+ z0$!oSRn29WeN*+qkap>-K!q*yk%T}oZxR$iQ*j}};v(A^YY=8TXmyiDMQ zfDrjTU~A@(F65WVR-ss*iGyoV`fnrlBQcj)L_+)OP2+X3OgLF7wRy6>g6)sHiZ1zt z^NidQY`e^h zEIS}t(w_twbmc8Dt`LkX5hE-K?}K?<#_s*Uj0?3LpfO@xChsVXE9CLxN_O&}$9N`> zJIeCcv7^UCS(Y-bOc_>1T%kXy=bmKsyL7?m&Oz$`g;fiKmNP|PH8~(-)hy`E$MQXS z`V9NTDmEjyBpnHYA>4}@DhECT0~(nANckpWD7318i^vs56ohP#c!kYnud_5-;N^0$ z*NVL-Px7W%Yu-h9QSR1+OK^Ig)A#Bm zp8#vbw_~ibxC(*rx^K@=pv%z^dsYD`r14sejxdIIFWATXB?~Y%&?5Q`caGd9BQ}Ck zYvpM|s5xcEha)_NsQ^%nYmAKM$61^cbfPhpq@?5oLZ~KF*d4dP&0%)f%6PSLxx06O zR-L?ZX2i*$*=w`a6Uv=GtBUce=3VDv(cZ&K)sL@=Y&L#zHc#;8@UfAT7Kq7g>eY|= zEbm12J?Y>;2-zt&vmA8rFlm3bRD3Di^&(_M%5)G!DC&Z4n)fPNQlSMn6P>^o{CR1FU_L_f@r=Z5I&1H z6Np*yOaCI1Rx>kvWU^=XK+yn(T19sA5nAEXmhO?nf>miJnkhS+vyi7;6IxQ$)LL|I zt5Ou-<}RD!eo{AREE8Q277_03hfAaFDRsIK`-f$3I*1 zy7-V)LlVMkqq66Sf4-=-AUn3@#08r-)|$0W99uDqH9VZNUW|OOMy)!0TJBYCz*|pL z*`0mrTV>dwCLvSOw)L4fov~#TSb#lO+3YK&M;zlttkGCU4Y@|^v?uj7!r)`EM#u*= zuaO}E{DkqZHIgYml>vyZOrfsI7b}woqw=20LInr9Ethw3ztj_ zN0zQ0D}H3%D9M%Z#-@eanw2TevE=i=^zU}%&-ZF#)mi}-va$Ba=NgLdSIlN%*Yssm z0-tNoSL~SAVqB#rdzS6dm#xmW-dHwX|b#y~|{$lhkJZ}*^)*KdE5|d4WD3`>e`#rO1cGzBVwPpW~ zq$H})YPe)kpH(UET^IBg;~T_{DH}uhtED%|Lal~ZHud52s}AM!3Hz7k0Dg|X;m}V% zx4(-yzo2nbyj)6FW2dD<%Xr|QFmuRKGmx03>L_3bsDT4DAj%1i0D5vNe=a_O1*Faz z@FZ*0C{|3~A)EzlZ1!tP!<_MKXM+w8CqMvVBxoJ3mIR_BltyC0pi6#cthUATWMmU- z%q}Z@P@nvuiL8LhWMJkuZT(4=g2d!GN=xJO~u2G9d)D*f7ay zD#`VR8iFsVQ{b+X;(nsQ{X|M$b<-L@;c}-IxKmT|>YJXd<8nV&;C?Oz<8n7Quf5@1 zv@SgsR%Y;~QBdW-Eq#-1Cr)fD=$W!QebY(x#I~1vbcYAe!!mon)T75sJF2&xdZI>| zk<+p=sfOf+t+tnF4`eg678;?~K}-fY#f;4rIs@_O#<$>QduIpYWX^vi%Tv!}55 z$+Ownthkp;X3)BqZdYE+YSVC_Tz9VfUlHw!gTG^j=G3vOh~*~x*4x6UEw0x%m4_C(cOt#kRb|q|0iOdETS?eYv;I4Y zY4PZmfFu8SXKi5|qg1y%)bMed9fM%p;O26k$oO|BrR2q#mH)^4V(<3<=J91EhYXoEjb!8)F+)89FKJm+{wQNqD%aJ1xu?bk`@KrwgD6OY z%{9*qzFkue5;hTbJe4OQjJVS+*`;GYK{3PGMVf_cE*Gv{O*wFH< zQ_VNGP3`l#%OH9|2al$3=Ct`i@gxGGJ&8<5-A;( z$~SuQJ|1^v-rnE#Eb2F7_44dta4KZRu%v4VEAG$za$CRopY4y`c3o!ij)so0*#0b} zUzp7utS_9Q#$p}p&O{|4EU}JV{-r8-%saFA=f&p-wtXpXJ}8v&81& z3wCVRq#bZ?FWfn4CmXhN%1+iw4BR*_7x{}qiR-2@$>jr zEDXs9fVi9#B&_)?7h^NBrj4xnM8=%44TcUr`GO;2=-rB$dPP zIOYYK%IP&FMatdOmG`3QN%&{lOGia}S6)}s^EjlvqCHN{;n1>8?=mGrw9Oh)EUm51Kz-$cM2P$nk|XGlp# zp=ege9+8MMlxAmj5~J+QX-`g6k`)vphzfHmBBkWSUd`&2!i84|Vy(rNhhu`oUe;Av z%~_-8#r48Jxj503B~=Su*7%IlD|A_tQw@Sq%dNM%F&OzB>qTg($>}fDrwe=Awc`2r z3Kxd1X!;)ihO=f(#V3Vdb8+|uR*ib(=80AIz0R_b8i<@>b{kOSr3rJ|TxwR)QQ`$O z1;4W`=mtth<5R1}Ds?|>qa@pJ`loCgitf;-l-C~l6!A3qDSvEkgpAFNdrWLjszJsY zN)6k1;1^3!u`Ln0Qqp(lkIfA(J#CEddo|C>!+)O&T8#CcMYehvbZMLYT@9 z=zt)_*Q|qv(1T9$vUgb6fT_dgjorq|CT@O1tY@9mn+_jNg*oi*KQ82F&K;3HKM777&TwsiH*X&-z+%W7 zvX0!`CMYcinShQ~6#P$B6BKWg6Z}1oy^Q0*FLpO;hwpy_rdAXCSV*%JKU~@31@Q}W zxGqeji!b(ltqkQp zA&uH(`IABFvP3H|FBAbq(H>t-8;ZijQ+0PBU08#^zTs~yX77KvU)FjTDdM8|4P8HP zFMapIg^#_xF+!@|`K@vczJW-p8Yq!#HXs-z->~SJhQl59p?M+DAw#B&V60Ha%%N0_ z)ufxr1%nm_%g$GW6cGm+tn#b~8u}|MlfGk2jcMbx+)SCHB)$=aSJK~`6|p3K#e_Gu zi3(~x@U3!Ug)>E;d={+8yhrj@#mLY*0#U-HE$jBLx6I5YBy(}`>(ci_!cFP8yJUMV#YxW$13T?cc zFT8u#i-?Lhi`j~7{56brCC&z+c*AXz_0d|+xg<10CG`)GM(cO$=sGlNoUWrXrc~pU z?)fU5BB_C%$OuOAin*;H3)Edw1yIu-Hx2M)OrxuKn~nQP{8V#Uzx@1u%c?R!BfP@< z9{b>(cl3sgpM1j3)0#hzsNX!Sd9cwT({ipKul&<23|EI1;GoPO2sui+g;y3AOFS7I z!31&FN->f-#2ASB(g=i{idy^1V)sdr$X73Cg}-_~=AG{Fg;?$~x!$Qf+uP6^ijh*K zoGDoEVCq~-L$CztwY+K=s?j3j20>dW_RBTb`2DsH=lQcsM(do>>o$s2%b zlzf_^$bjOh^69hllMQUo)kYOJ#vRe#(?GQ$JK5P%&FT#813Aps>9?fN62Fqf=ko<2`YQ=u% zPl)+41BmxypR6eCsb>Mv8E}hRPBgy+7JrBqDzW&~vq;X)lFM(2{Tk67)G?3%Js=Vx zp-moZT4hshAvXdLrf3Eh{V}3LijShPX5leG@5-PwepGstCMilgBEEG{hEW-ezO=87 zn@>hdPmQr1Mqpmf%Mg7Ui;_AZMh`cIiP6yL&3Iwde>3@UX!H;!lv*qcTKj~F6exeM zzsD*n;g?p_m^|so`nU9iC_WPcC27-_<(9-PpR#1Ds9<^ zI(3PiAbYrq+4o`yfmpbkVvPrWtwS1&KuSskG}Ku*SIsLA-NEd71?5yPmf~6zuTDx= zQgzA4fb}*t{&vT@@=}Z63nN5U=d0xFLZD|=1BFz_??ccX(~+`W1pLx^J=O^O#w&Xl z?$X3J>&9H3!NxATbG%vI2{Q&B9ed!rjQ#^Bc&CgXlRj`lpWYM8w}9%<TaV4lXi`en?O78;kzW1lK;_sf(yjJedEt}t7a6j+E%P4GS8-{u|tI<`h611qT znp)#2T0v;hMpWB!DkY4=po!uY1V1%G+*nA7+`vM@U-}5Z)}&k05lk*#94#_DnSK~6 zXo^Z`6_YE`vw(7$7Ts7#xooASds=3v*GUTX6{3s?chVw8!5v*I2@xN4wKjk3qWNED zonFHdf5U$2Q^+dmyWDt}D&+65EAi=PJ1y@%72glaTWGf}QcUR(vwJ0vgeu0PMemAtr? z*x*=-_lHqMrO?ONtQL0c*VP_j2q07VRH1<=Fto_qY*8+QDdY`_NLCfKfhC26%G|J> zNO~A|lD#If_=hKROYeI4sl7}NV>u6JqPmJv^Cngtw?|^+A?Td~;VciiCFWv)$4QAb z%K{7vs5>);N}(DB5PSe_>LZH4DEy;&O8ml4-DiK~Pb0oXfAZwWtT*rds@mP6=E@nY zwzwP=&ks-;pNc`he1GjTb_L^`4h!WP#vO_AIehv#jn4uOv1sc4V-%t{1_(*9l%d90 zeF|v2Z%n{CVU(~_^r0%(@|(3^?mmp1zdg%4G)2;1Sm5h3gWlhd#J~L?MYA&E#}c7P z|6{f}&_MIC))d*antb_ z5kQdy)e!f!bz$FsgCPtN4*EAKskC+>g87cb^#&x$)BxkY@0cXp(>`~bG| zH@PRzoxjY8bF4(+TdD~iiQJ1I--vx$nxXB&02nbH#YYR-Z%EE zeLYT34qNi{cXO9;-3o&Z2A-J$FCrpU;Ee#`!x!RGM;9H%^VQ)eJSe5nANupkbod9! zztKt+cz=X#yWhX@FP>>}okPe9=fE^r6Fe{Nqsb{3eXKq^iIr}LL+ty+JEs;=_^z4RyvM@GNuotM2-o zGcas@igNU@J@_rmS_{EU_O0W4=)RmJo{$sQXR0rboU4flNDs1V_U5@pMfoQSBgC^a zw?VCt&<5d6nj_0dFG8UA$aUJ;!0tSmDii3|;wt-LNfXjq1WmjyX@Y#mgeCD=i|`pw zlh1D#s~q>``=8Qj#eP~ozXs1c<@^8i(bh+xA#3OH-oB1E*h7Ewak;LB3^5$Y)=s>p z{iWSEb8PT0Q=A>UPb`{>W)bI(Ps6cCED23HP_jsGK0eLpHg0V~&f@j%3V6CL|C7VoCV=n%*GVGekqX^-n*}2RuL#xx0GD-wsrpe}U z0|sOYZ{ndZWfFl{$puv8;uy)jB!UvslnUOl$`B@oT>C-%`Ngd3V^6G}G-uP6wJ03B zc8Z7zzJ2E1?*|>r9=USK_%#c2K;?tP7_~BXD;gDnO8P3L`%3p2cJ}{1=rX3}u^6<> z9)h0vm_ZYoQYqz~t@LAJp;;ByjmY8ce$j%J8k8wyB0EfmGr{^RM>Mc zloG}Jo))d_!SY*loxFpx)u)rUQp?NF>pFP{z8{}NhX!ar{qTOOY4@_G>iOredW@`J zixy-!-837MQ3Am1p$D7&rO-J{**wIsP&K4tB?6j)cOmQsWkcEYj`!9#;-8`~*f6nS z{)TmPl~Mee2krj2a273ss`9$ar)95W7#nJqIY9tV#tQM`mh8#*&DOBwzs3i2%6+g=dB+$?m3MV z`k(E;{r(I!{{R{|u%~9-y1D!K(r0l#YZrZO{{;J*Z0oPNBzjNZPpMZM`)MRjX{@rW zT!{<_GowLF!PJ(QcPezEis(R+9A){;hPP15p?TFLTA{7gk(8uz`v7y(~jb|dC$zTirQq~6J?ls z`_SuioL4{KCngS`GDbXfurjA^966wV(wVQCvtrAvX-|Lfo02*Gg*D6BD=jgX37N9K=DWX0O;^0mvDypf;BHCO;TIt!PZFY{z z8|H(1C_lXW;JuT8oS!HwXk4X6Mi0FP49KDIn+Jv1zjNRG+0CW&saPD5nCTR~O`l?PLQ=+Sc4 zGF5GIxLh^PCtaPpbAJGubf4tM03qDfFN1%OfABg>KR;mG!|Cdw!z`H$6S7sXCW|2@ zTVtoo+cs_R0fK z_trxvgFG~nA>SR|x;MaA6c42nz43j=@clb?U22~<;Yabidhjq-VJx(7%CxR%YoSH> zs+kbITiB|h>riCfY(hcdF=Zxb<$Pf_JL;1she`mUMB5XD9i9*Wkb^n*ye00v$(OMb zn{T{cf7ZrbSJK@h7A#kkwEUzn#wXp3SjEchX0AOfW=-e0&kJ!`O#fsIUomUt#l79S z%|15_Bkx!A6Y9sb0s6C<7G}!Zx1fv-yTQD`n!TifGB#O1)VH{Hom$DMQli0g10Axq zkuE?J;4k&+%EPttOg**xhWKslr}I3$s`r^Yebtt1-mc5Uk;^-+*v@MH6O(g)ev`%Z zZtRWSc^#unD7tNb0Vl5vDiGRD{dz3!8Gn%HU)Bxzoq-)Ng?O}J9^vC0Y2UBAKrX!%NIo_O=s)+5G2!_}&mW9s*w9eV>%DQ~%V zf1lBO&77RKr_66q@T=!`*d6*^G=ihHrdK=k=)C!`cEYx9X z1z}2V)Lcna%@YAX7>E@1Go@`|8dB;|WnM=@9F+G&N@lBsWGz4X6l%NdU&%UObgz2< z7^mUisrB>=;X;(?_67QgNkaksun#s@EKLv6# z2$Gp_dXg`>Wz?2d0B%}1`s@W|f#@uQ35BIbQ0{s}B#vkrdt>yeTV2G>ZA!Zqn!1cN zi4gCym%B~sSik3C`qe}p%Y#rG-vO`n;~sC~^V9`OMO!nr=bG)s4;s;X-r)1>=k(`h z*!uTxM3zaA7@};zcoq1|U`5)wQcWijxEJCPTpX$_qdzeuX8`*7F*V-(2a1Z5BZ%0`3tfjL@_kR!HQ$vkpFF43@wSJG} z_4GZqK$)&Z0M;;QdWhyY94iL(P!0lL7}-2~@#5^hXYoCi#J8-zZ9l#z);}IfqO})y zeuneW^ey3z1%tXNo<8E+7FkQi_U$Lp8*MR~0dh1z*&^sGw+0iCdjTC2eW`j3ZrzhR zY)cnsHE;Az@7HNP2dYo;!&r+XHX{K3MmB>zC-x)=*MJCGipO zYx^5x4YfU`m5`AJ%7n4Q3Pi=Me~p>^+UQClZE^o9ggc`2X%W!1mR`uZP*D>c4J@?OA?MDyqg%Bp)_>tbRF1!#AR=W>uRsSTk4v zO9otWsEoUnhHo12t9XgX07M29O=G5@pjt}|&KIND0{Y8m%TXw9x#-2#=O`uQ1XdLO zO>>ybqj*K-0#0z0Rw9@ZF>Jscxl5Rg6CI*Vg#e-U&duoFZRGG*dt@l;E1AQ3B)&2{ zqcc&;vZB9~6x${6j?(Z40CUbfl2Y)z`l0{%Wc_)1UFi&pftAHMtIj{c4wluFQj`Tj zzn>RaOx>UheFSmd=_dFF{H8o&kQkoLhV;L{P{LxlBy&i0b+h{j)T zPu!JD?usjNC~&PTueIgX$j!2-p$6i?jnE60Lg#5Hw~aIg#5qt&DXzdBm!efFO}!x9 zl~S~%vNDw!2xa6gS5u46B^(qrO%JQN+)o#{Yr5Qx@CB~C7fdff|7uhG5a*JnOh`=t z>ZL1OF1`=y?b`~L%%V_Ij$GtqBEs=xw> zZJj=2>*g8RJJ_Is{OrKJom)2V^h%2ty08nKThP7cuRLthx#bJ5bZXhWGwU~f`-BPG z=Djd|`}px&=P8q>@7OWB`D;72&MPe6L><|ruuj{4eLA*m+qX}LmTmj>e;N1sw&y># z>6hNIMVtQp+S6VSwXNWB%6{yZ%La)6EUR7a4g4Z;zl@lWLQ_&s|*BIg~n z5~fU5i$x=STZV&2#`_|D0CmZZ@BsBL4E%A4Vuvf{g8hyQg_VK5q)HYKVp^hs_zq>k z@ajA4J15N*XGBT%ooAT$LnRKQ=?3065FH_^*j8%sCf&qB%Z8z-RnfbDR?sG&{_=6&kjX2j9Go&6dPppoL2Ynks@9PJ zUR1@lSKmuZCYtng7+Yk31d6wud^xUCvSSj8IE5qS4>bqEW5an5npmY2_-18eUlO`V zzqEM%W08?1Q5!CweX27HpC$yHwmII3_;nQ+as^sa6dC4Pav=5A#G*l|gX07tLWy0b z3T^m9Boxe@#V04&Q`6!TQ`QP# z(&$TQ(Rt+(8uHAN+2#iol05SV)8L6P%UOtBQ-fVNv5DeB3bgoesV~Q;n8RdnV-T5D zNFfc`1S7&infjY2rG=uCyHkQiQ6q%#2?!m7H&U?GF3#6(oKTCkXf}E3qVU^i&w1E{ z^FN0#%0K@w{lvU^ug|!k?DY0-+_=>8@a1#)x`Ri#7j3^z96*Q42V%*BoZN*Rd}3SC zod6eBC<*v8l5wx79px>k&Q1|=qPb-FziU7O)w2oh>G}Jvprz>#@J+P~ zw5R0*B4XsOz*2Y$-yuqfa((|9>?q9!X- zM){cC5S4P@$rUl|EBeD-dWPL7JpT^;6RS%&E?;&yFYr^&-SAW)M+5t}7WPdL_(p#d zu^aR)`MdGJu=XUs@H@9n5l+n>>a!0(*XF%I&m9>x3?X@^m$3nD%HTFBZE!H+1|-b$ z_iR%={W8Q+51Zp*&yE7gRU*LKV)C}p$t(qy=EC8mYb{T z1#u=>0G$+&9DpToNUafFIOZh+B>{5S$ID8i%&)H~0+@e9yz-KmrtaH3x_|ii@%O~n z4h{cGunK<;em#8H@V7wUSz;QS#w)ygx#Id5*tVr6&zZs;EH-vy!i6JK<{)>B&fpQ8 z!LsnpB!Gh*(f3jAmci=$K=17rNsj|!xf+m+)iH$g5tK_N_R|(TICDG^%NXx81 zuUBwYh^s);H%Yzt0%WUcnelM6#l-{IZsvAzE>a~?$j5<0l!7=F>6DcSHAh#hES|v$ z#T%rs0d+w7`&NpIvyhP=Md!#7&yulPRKcz$i$~CdZ9Z`Z|9IKFpK9NESK;q`J4DQ9 z&#T^H{LO~m9jwbP??c|Q!&Ij`p2ospSSvwF_o>|m`|bGoo^I}znng?UUU z$7tIiecdcdgr5~b(wn?jWak5YE5}R|d_)vtvdEGTCZ*ZpOA3;C37BL8CgudsPGAuU zjN&+T;_INX6)wv8JB>omJ(*kj@9XU6uf4yp`%_j&c#l5G!)p93Zl@?~?4@5CvsT;Z zJynVin!<*3UwbS-c9D!L`bCY#JYsB%^?68_va$b0F(xf6R4Sff=#*GM%@S>BBxIQx z3NY%r5Zug>sJjHVN3*ensh}6(XavbwV)w9(m@6XxCODQBW0%hA7&`G04r&FK9O_J~ zlZHQ>^*RyG2>gZCE*+YtM)SqupFQn{-o?KY5ApBVIYsGvS)AQcns?36GFIS^brD6Q zv{5VY=e^09iv_}nVnCYORJ2P$47rD9Ga)18xi|QhVOmPQWf9VT;bJQxIRbKpO};;x zB)C8XDRU_I32#Rd9IXH(a`JqXrqUt2=r*%^_}ZnOMeDdnVa{6~WkkGKAU-?z-YsU2 z^VZ_$qbPg(%ZxJ{mPNK)LdTcA%`?u0?0iHKk&l7An=md2+RT!Ku61670T1-A@Vdf$t*RV5%V-P<(NR1i3c-x#8vUk3bQLlCXms_;y_hLOS%uH!(Zk^}w+m(Mx zdkwS$^o}wZUa2TV%m!#-I?58f#=uNSLNc%hhJQtKl#*ik5ki8%f|NFAPlB$X96d@h zcKALV32ErGK^}UVIp`nrRt)IAlfVDY<@Dhrda^Y$$LD;wb4cd(E(2;L4NxL}{HeS< zfmhPb>|UC9QY;#hzH$NY)A6rK$uGZn2^53ON#_~+3D~GE@Ga84V|Cpd;uK_#t17RC z9-a&$Nw!do@xYQBT@i2dSLBN`LZePXy|_94Ptja}w&k&63zU)}!c7kmDlJ$1P>WBD z1953daYP?4@BWca<@OiJb;vVS0)F<&ULG^4OEbgY8mK@22bxjDbq{H~dc7iV@AC6{(v#`c*Td=_q(*}j7qa`H+_IP4*pzAr`*K#ADuTlANsy?vi8na2Y4wTwQ) zHv2w<0Kf4WBb+Zk(+8hvVSfvsiMIvtMe;Mi*vsG9QT!PLyd^)=1!MEd&m`DleV>W* zedg8T&lHah`!X2O$eQS>Tn<{$t6GHKQ5jB*1aN3ydV~>$Lf;gMgTe0C6q~$}2}0N(IP;Q$(r(aWOTFMhTY9~=%JtOf z9Oh)P<8Pu5<2`2kX8yW$a~G~t%db~N>*wcRn@~R|qK0TAhSxkIKHba`{tmvuT)BtW zyLMc>fBl(|u7jZ=Y%i*&ev5PC1b?BB3>^>u!GYZ-#i<#g10Xr2a^O$>*5j@1Q5$?> z|M@|fVQnd?v$snj&v5&xA}2u5J{%IZn)E7J({f`=)O2i3@U6IV-%-j$`Djy zo%wJj@9kZumMuKXoAJipmbr`g`~33aTyGQn;x;}{`3Z3y1!qSNUY?IK&vwZ(6(rVq zlJZmGQRO*4PsVZ@LL}#6qye^PG{qOo@gctrx{+V!*EW2hKd?TNO-PT8(+@cj(3XajT9zp$Z*WXrQQuDqr`120URt#>2a(<{OF_7pAmjw2Ki5sKv8 zhDjrN4uqs$D+Q4P<%Zf0pRH&c<^SyG-m%7KCC_-wXF(IyaD;DwPuLMBsH+9&ej0ea zjIP)J_VANzcGyTc*8=1XrcUYUfhs#PADvw?7l@(g9Qp$ibjtw#lkBa+ub|<{@7_On zFwJzoZ7W-){sk+cH1eS|iip_~)k+x4a6W;j!kVLab#)WBGe$1?Ha&pu0E+@XXzUF| zEyGCP!r#LM3s~I+3-|fboMNw$`drA}K*S*=0Za>@ttRF_3BtpHgS3|!w| zn?>%E<#Zc!v83NfAh7X@p|;Cimxe6aML2gRhEI~kgjyn%4UClfoIpi`)7;M&P^Syt zlhAl9&iS1?FCNX@89QafxIv$P&RgW?D?1l;-+%s@)is9R?(3IB#yPKPM}_|ylV5!A_HED3 zo$N$jKUU3S?-PD`2u6p&j@^CenFp0g8>Y}_O^lL!&g=AX%9S$4X<2t^0vM|+r(-;j zBFT7A6_!F(<3SZXs6y*b6HUMaYH%SRNF#(E#JTd~O^hJ1fOLsR_J#UI{d-4{e22;E zUO;0B5Vyu2-QKw~e@CN+B~sG5rnANRc8wf2=;lq2$W)hjJXnN`w~vJ`4;+*}Qs274 zLuu=jopSs1{gW$G88JsWUdvKQ*tIdo%9vJVnh9{#W;A({oJqX&%;B6oG9^$q{6dvw zb-ri_3yM%Yev-Z&rJ!E%EUh#2?XkY)BGWuQeY1;qzpx|C4wl3!c@D?wvy&4tc3pNQ zF=l50*ZaLb@HE0*n`r6&MXm*NOa_N37(Pu4*c?y>F17; zRMq9hQJ2%FpXp!bAP9Qz`X1N2p0|Ws7O~j!PWJi3UYL32?qg3^2fRXhvmG7K75zoq z2U=w7HsG}SWM9CBy)Jjp*5WE=x7;r}Ss5yYgxkpAd)%ES}FFPBAOuQ=koGjl^)H zs$h7eDw73az9CJB+~*X=szmrNIx_kVd=ROQw&%wotkt^7*cnHT^iS{HJvK2W|F!gf zBL}mHUG2a3WWP7UW1ly2P}T@>dvn___w?#i{roD=Ia>ay>k#bPlEbqXbsRaSbF=z` zGAFYeHt@u5Vx;pI z``d_@M8lq_V|xa8ix#M^*U8ou{4yO9crdK%)=t7? z-0f2GGMs*_hZfN;Lw*P#zT#&|U?rL!LPD6akc@2u~clR?c z_mBcNCW+Z%X&SiPLkrvk3*3DR+zksrZ0^Pd?q)7`uL5`X0(a*EcdLTDmZpz)EpWeD z;7$h#quj5{-3F6z<-KJ3IV4MuH9Z{Xa$~V3y4*7h+>=~xEZJ0-drpBn%jKS5;Ldir z7l6a*En1WK2#mkSiKO>)yju5IY`*qS_ElJ~RdlWdJB8 zM4&7yh@VA3fv=SuMuwp8Pa1{3`(gAd^5dA{ti>&rqnGh;+$vAM*0=vOK6PNfY5ch( z=_5PTtW_WuwQSs|Wy>awS}6J52ff^RSdW2KYTrLK;*`De*^GO`SlTK39acM|u=~^j z=@(d}w;PM_c8B>?qWJ!i9%Jh@=u!Cl2>H)hF+=`iAE!?k|LT~YlgGa*_VUCQtvkd8 zHG5(eYu>a?o2Hp`fs=Svn;tK=jX=eY zv^7~48=L1u5Julo)W?nj27v;&^rx0lju$pAd|nNSsnoLj);AZg=NEXrYEReg(Jkft z`SYz*m8+`b<-(^5pHeRVUawx6ar6E zmPpj`3ozy9N*fR+y_DC6`j;1B3Ni8mEj)sXT4_qKtdvd2IMeGmRAho6jYih)BWgrL zRk*3cLwG#j>D|t%4{Ow*)eCF3vVfICI=I=Qp)9(8)2E(!@#&GvvW5>F()JCEZz2@t zUzG@;lxph&N7@|5z@*y%!b4N-GH9rxu&QYYoPIw6F`mR!_UoFftWRm)<>_f-NLy=H z^cP=)c>`%@YpyAJCtp~UMkPQgosFL{91_nrM1Q^%qQ%1U(&EWS8f2a~2`)YSBc zhPd8M6NO%YVKD`}VT4$MVo;N;!Cwcth9$R%h;Z5LIJ1=$M7j-`7s+q++xhnU-{znB zLIAE(C4nuw(sp{xj6nmlCbK)kuekSpBfe?~e9H-Yu9J;#+o;KmwTp%hn}<`;tLQgn zH^v@;C_r^vKYjGl=WC4KFZHVyxT~gUx2hrmA{G~OVx*>2B^aL;T(&B)Z|DG~ND+)( zvqYE6Et#DIQ4gFODUjnL6`uen#^=9}yNmmDOds{VnDN7i^p3qgW$B--o<4L4>Yxvs z7BjlXtHU#nWhz}ey`Ge`G(Bhk-W7e8Bqz=4v~15FZ|6?Khj#APcL49#rf=_;+NJl= zPmOXyT>z@Ar!gmGXPAi7n6N>dZiFFCV06u9Cqos(hgTkKRypDQ{#_pBZKh0BD?PlS z++9KRITGu6SY3+sjDvq#@2yClE5ko6kEHQS2JnG`Cnk$b3ka4$7SwpN2hd41=W3Ar zF?mNZ97MDo%07}y zWX!@}@C_xb%S&$F00;|%98-!XM7amL@`i)y ze5)zn=DnNPXVjr9w@&T+F-QxZmYz9b{-{Ass_^G{%Z=hzjm!JA%XvB7I%SU-oK>|P zcBKX^(Qhyh>HywO3l5QOr@`m~!S4O$r!gPHxu`K6T8?~Dixmo!hFGK)Bwxk`kW~!3 zI2o>s_)w>kwr5Yi_mIMyyz4#5W=69$IpP6xj8ekAE>@U=Rc(V+U57K5WLu=gnR+Bj zaxg|=q%3JHSR!N*cR5bw*!HmFsMNOa7W);2-1fz?6uR-%Xai}O{pgq=V!PNCb9bBY zM9Fc>N{MF$9nw^!j_l+4Iz{OnZNPs}{zbkVpTK%e9Q)eP%=9eJcYBX~*00+qKb$=J zqi6ci%q($d1YgysL;GIcTF>9NXLZ{-iH$C;%-_4bZJ+dR?~)It4R+!@=3WMJE6|j$ zB6*}ScfZ^+M-(Bq6qh^~V4mhMH8kH+Xh3J9P4&qwQuM8pK(^lb!gXxtfF7e?oiH>1 zuXhi8+GmwG#HUVYYhIn#_~k*FLpFby|K>|?ZxBD|UOXFLqb>%gX#Af!g)vS|wMi%h ziLqjG*0@5?N>OHqDC2AL#eVivw6dqqI)rVwtqDE5oH+6a_WL~~ ztK07$xXl;oN#p3@2%-*5IICMAUiOI21-vDH&RaqZVjKBS^!-Wr{-^N$C2j2=^Zin9Fs5gG zfAPdgSJ8h=bI9LB=!&ABS|Ac@j1fRC(Eq8tGK;+-#;_GP#3$nF4P*?Ax{l=Bz20Nq z{p_%4%3E=$Zw&La*?-4~N+YK@IE-{LePD8{jj>3di}65u`iuvDM+9w;QD{eF61|`~ z^qU}gg9VDJ1GpevFhcY!q~4c%N`~Cqd^f1nnM2~!w?!di;_hy7T6kKre!^hjWRnhFDdFFMeBAMy3Uo?nz^*4SILO1!?u? z5Bh*tjPQt2$|uU*!ctNjV7BLQ!av6ePoz3_{}MF*G7Y@EjGqTeUkQ{ab5H=Pq3L{+ zjS@$e$|E6B_%)IZ4bcZNDRzHU^096uVS@Oq)4P8jKXu=;V8V!5lUQAMr$5hL6u;)L zoIf?2Rc+U~WwrFZi?sJvrw{4Uu5ssf{bqMty8Xh&{v!dC=kP#K57Y&=AXpuJd{wL5 zi1AX)hbAX?<6lcguNc@jEku<(0)Y?HG9nvUM(M0|BaM}nwlRU*e6lHlcNIT)*p5$b z|HR_Xc)$63%$!+Me9lX+(Kbx)3I2(i#_5sgsXUbghfw=EDbNQmD<2zSB zI_q2Sa#(MwxYAZDp;ugi1ErL2y)DPj&|0;UdbReFmL(=JVT!fU8G**4YmA8oV2eHH zVIRH!$B&VUPB&I%Rtsx zIH_4`nGGHc$Ap2za9lRIjxIo2ZBb5Ldi$IpsIGkH`)|c<7WM59EQ}xAwQBhuch2hV zyb|&>FR_};`7El<)e;3o_A{41J>BHpD_A|`WPw^cWAy@UO?C2=JjozW3mY-yxTIoZ z$Qrj3YuB9ehNL*H8DVkM&WF64JRaWTkTSGz8TiRkWf<`g{GxpvelZyQoJk#1|6vRN zlB~oSg~7k%4;%C+ANWrUlV`)%T-|q4sF*fF0ij`Fs9_}cCZfCU%-;`%iIKC#sIWsA z(g*`1*3UZWycw`>R8b}4O9PmbZc4~UpaM$R0iqL<0yFz`smGJe7JMz@Is1}3 z7mC-_1>R2vUUDzks)4(<#wbEiE#LlVCXunkh9sh#92j)gnnT6zHL2S2C5J!G zd*^!oh>_igV+~KQIeqZ0SNjj^iQgh1#7p=s#pa8lTK3T(AgSx6vX}Dn@m77kB@LD| z`~nuKVmT?sR}^znVnu;A!Xi#ae5{PN2?evwmWcc@5`4bevd~O|wdlp6k)Y*fxSn5jRYR z{Rz_njlxLrz)$qL8;%0m;Gkx~MBrIHN?{K*ru$uR1R5KX~3Vuvd?PstTmr-HI17a&x0TZ&q=_ zq;q%PKbSdg(cDRx3G4=C2`D7WHq3(CwwMooc4{jnv5NuB$N6Y$) z`5>4=mbAHc8Nq~<111{Qev#%G=q^g(l#@fFC zJ`7V88KN_*w+pLm|1Zjd(ow{+FJaFt zSqu0tb*<@njLm6lgccrTmgrk*5nRStEb}MZ$mA#UOQ(4C`FtQ` zkt#ZAOCByu2ZBYo>_||vBWn#Pc*XS{bBUk7_WUURWUkvD5x^R*+^p2zlAF6ljSPBl z;e*1U;lqQx=ie_38tdIJu7>iFfy;AOycy_SSOUeo8O=sLfZ0S)-iiqgC>PLR=+Zr4 zJg~5O!@aMCDbff9f)XJwxa`fv8Utt|l|rFuAYla>2PLnhhGHJ6{CgJRQU17=f9*GW zJ`2xhhf~=QDC0-}6knAQw^(dAtMCWf>0J;DpZNc{d-L!riY#upt8d?X6GHZcu!pcD zf@qKcQ9wXN5Kx0ViYy9>0zyE7pa>`k$RchK5C}W6lUxx^a6ttXMRCAk97Vx>7o5Qj zxP8ChIn`C&9pY@y_r8C;GmiRXyz0CIsUxab2$vAm(kS82=I>M1u;v@@& zqH$0Vr=S{J@Yy_tn_8n=mtaO>^*!U;W%s%`} zL;LNe)HgIY_Cv5WXkO146{(}{Z>%m^UGKh{9i#;51s)Mj$zwqz(3`XisG|tPpSQRf z-0op368IF;P36NA&i?j}yB2FAX4^{x58F?cE`9&AFOSZfa{tt23oGGpz0h!NUI#oW*?RIXg;)K{GaSB^$IiOjLnr`;_^t$o7`oUm{E@z)ZuR#-2Ic(Wml@>kBWZ!8%ys95?5!tt1E z8nW-_;wG#{#7odp(rY^JeJ(^jSK5GT&qW0H<(-6553DU>4iQT>>P5;sf(hPSLFgl1 zofjnX$VB2+vQ0P%hp0fNig0G0M+RjjO_T@_eI5sd5Se#RzkifD+YHeB)?nU_DHE}8 zyyuqwW5}L?pDuVVOFQ&=^+-G4`K+`(UQb;;|0g@oQ3NYIl7%qW`z#A+lRtv?*c^Kq z?Xf3Ty#I++b^nxQ3vsCN4tonFNn+?s-eACTHo8VbOTy{iSNCI~YyUuH-$7dA3D024 zGvGbMp%~CP4t$udu=y^rAE>maRGK|2?Iq$in5+qbUzDdV4)3pxQ{k3KfawI@L@Jg% zX?;6godCq)9?=vDB=CWN<}EO7ysHi-_pCZEvveiVA`5106l4b$M7N66cfa~#k9~F^ z!G4X_);<#W%|5z&?V2}DLY5cRzL79K*WPS>4UCid97~rNBTJfmi#TwJaz6i!;#@Dr zMNrCYE%F|UKr-!mURjvI-n&4r|_&s^lls1h>1 zji)tw%@TAr&_>61y;kBy37?3Mw=`ON7%Sk}hLnAQh&LP#C9=b72*9 zP$;5K3kNt<0(*v34Yl`tCuZ1XM@8!!t8NsnkJ?Mctnb9Hb}jnPyvpos2Sk+qi@pI5-;arI!E%3 z;orWyYf_rijW&^nb=dcS5_nG*0obg1OiyL^!Sk&m#FW<5ArLqF{!tH9{rJMcpGFNE zKgRxR{|y&TtPD<@IO}mcwruIKK;LTz^ecMg+4T#rpVzYa|M>+XASz)WZ!$$rf~dflOSxsJ81a5eQ>Ba1Y&!W z3~F)t9*`nt;Wi)SblW<-vT~?+=!o53e1D910=6D;!=C6?;D=My>cG3SC}RDY$4z;n znVnsyrjIqMsqw&qc4NSFj>*Dwt3xGD#l$t`-K~}b`zA{J>4g2Dz0v+MxHb?j`^Iy7|0bP3+qbnQbPRe?5I^ug4NZ+!Mj`It7*km)VeE6wgaQU(0x>3)jDu`dZL? z_cci|(qQ>QB057M>$`8k1wefv>#G)#G;)N!K;EbkIKmX@>5H2A*u+NxWxhocWdrm? z^pta*d~CSEj|cNa#?Me>A2?F^=xUsKm6LJiRf<>Jy!DDGwwK|&GZDmOt1Qz_FqgMH#d zY1`!Q@F<;;T4B~QDT&snev)I>uWCs}6X~EhitETjxEb2)9{OUp-O4Vfyv*sZ-eF%w zDkX|gN8tp9>SFiqx7vPz)JUAds7;{=#xUTVYXW<8B>dc}gWodpNBV^9{U0YIQJ71D zGZ){{tmYDljiA&+|RwjWZe zgE0EQnrXn+%>6!V#+Ss_048Jua?%(LsDrHUl!{MpfJf5Dz5*WP?StcshfW7H=kDWz3s7Z&ld7NW2tWy=d~hw953~ z_I)8zgA+bmupc{m;F`{C3jJRNEX z-GwsYhx^$6zV-)|l^-ODj1OxH;5Gb*4QO4tCe<}Pd6+OoQ#P39q2Sl&&a}dYT8#cy?4yv zH^pBnf!?`ai3awu<7Q&+vO88rOvKoZ*g1hV+buB(axek-k`|kY8#08`2a@EVPZp~q z8!N%VaEM>8?1x7H$#L*7`8{3EK@I(eJ5v^zGe4{R^i%Ux^rg?F!d43BroDosakjlV z)D&3MGd4SQWRhAwPVqT`e7+Z*&qtWEo~LwEt=DALqI)GKq3a9u3$%UNJ~*p53wdb# zYmC<)gPJ_<0S7JIqkMqyltGuA#jB7-&UMU>)gz}XfKPd~hKSt7Pv2|ru0erCHb7^) zj<~ia9Tv2hgERR`&?3cnFalpGu-3)azP;jekSXS2PSTT3jQ^J#$K!2{7;&8X=%2KI zhOBX!7m3kj>+iJ3I9iu^wcTA!Mk-#ko}?8LS7jIzBMH0*O@Iv#mVK!t z`zE2-5vyji?q&5`3~O) z`&-K~hjWcZk&OH{z<`{)>c%}E8}T`#3gjzVvf z>&*0Ad@Yh=(y#IcDK%X0NtZpPq!s8U2!*);nFdJiPTcO9lGOp8Hn=hZ!Tx_=_wZ`m zOZWPT17h-{@4eBX?dM|uv(LY?W8nDc;jsYK}7$YOtOqhwj`PUb&`}5tXTsz3dBgai7LB{g}2^sK;0b{{gI%;jxbQYnML3{vRlmzIZfc=O)mmtEOT~ z-xKM(-M5m?Z!dwvw;^1|KAG98E9?8>JWOS5Fr5|{*bm-Cb(t{}P} zx|10@9vlJPsUa$VMn*DQ@ZLej8kPCb>E>H8wOu1QGW{N%=w7BKbRG?%9MR$&&Z>r_ z^rq#(ZST2l$8&*X9lk-5F3jZcJzba?K!zCInHhL2X1}}c`Ne~756=Adk>5qi>8-Ck zclpe|7j%Ap;rwSq=Hjg>qUk>qUSB`(p4w0R@PPfj9euE3SN2(tpOtT=zPhHt%8i)e zKxn^rVup3#&4-o2-h8q$vYZ+6q}1XoWBgy9@`XW&W=ZJV)Y=o-SCC&<+}p+ytet77qD6?dnC_@L>hVStm3L^#cf(t zhZ?buJ%qL(A#`@cj{%V){ENcfBEMTa6)|)bMZ!V=6XAvm)01jKHUFNEXed^#FKaU8 zqEIbKi2D|pTvCqUM_u&66&wjcmQr(9sR0usyo6|AW)5f}Q&ON*%dX14tQ6$bEwo1k z#C&^Xt>!W_rEA9WwSiS-rYAMUTt9I@%EaM47cEL#J~3s$#BM4#h2-ykUQH`a+>6jzt=N;m;@gGDxDlSKOm6c@5WE`PfR0n zrsbz~O)E+xa;AZt$({kwOUbhZ5aOb?wMf!wOe$VZT25OCD02M}MaY#36P9`lGzoxQ zsL9Ez zhqD(nw2x1`3yJ~x7Q$3%raH83`cFEvDH^qa{oX=-@L7FLos=w==S@_k7%Tyrpz)MR zP1?2_W{OGfPd8Z&HrBsv)0WqSw2Ry(4>APshb4!Ca4!`r}t8cnv*uc}z zdXh;^igQ`FLmpCR#~Ber-e{ zS(R_<5-6jd%p8K@vSTWmlCCh&#F2`f+?q~g#dpnz?wA69AG7hH_j2**(6B zGpq$yoE4;o&AuZ>EVCaDiHR%i=|@t<*$-~maJw0T7Ms(Mq)>lQ`DtL$z!)lVU|n0` zpo}Yu?OO3}DSkMS3&l{rSI(K%e) zR(xWwu5(fEsqMl8$CR6(qofv8`7tHmn+T&{jLY* zn*xpD`^v^$6uiY*p;5T2BZdrD;bP+HQkmgCrEZiB+>cm(lw^DG#BT0TNXnVaJ8ze3 zo|S5!$hvIo_?gS6&w2WjcP1BHk$O$n3$N@MXgtmQ=!(bg8TZt>(t`KC2->4sH)(q9 zbw!gd2emt5&DVn3A)`QI%nk&FvD&Sqj94x*C@xi%Ok7-QuC-UM69eo{<_L3V^iuPQ z2O&3$K#k?tE$Ptp&NA+oqqB9dMrV7PH0b6bZkgLWf5(yL$C}5`hj~YG^X%sN_(vp$ z_RQPq6K*fd@}OWgCr?eJc2;xz!f{&B$o*qE?vKlMKZ17XR{#OvRPf5$WN+0jwc_CF zQ*+Keb5_{LSM=_7PwD-WOCP?nOV>V^PriI^Vb`w3UAvi=j4$joZTQ5A!!N$2LE-pe zNu6@Uj;&}Tu=bnH5&4u8DUa>#S1(11W51jER%$!qHtT8m#Vy>0xw)ek(pG+^Id zxN!P?16vh!?sR!?(e0wylP9iN`{af8&Sm4#jRkZvcB~^8d6QBfmN3*Ei14oAqVS;b=&&_F zo^U(5nnmUynl3V;jQnn?$=EUZ$h0dm1{tG`DMlOsQ((T-LwB~iboixvnpVIKTF?Cm zoX}A^EX&QgqPS3&QE~iLRJ?e2?1Muu>ptX`9+wThuAq6dHf@@){NH-qGO$^N6giSWmF4(R;d6)jvI>bUJ%<&M zPhQl+Io&rze=tX!J{)KqU1DaKNjC1GMP4`VAw>x^Qc6agvIlI)~7_ChD+Z+|ptMG_9rLRYzrG%Tc(D$*p5p%KAu z+Dz;7R<)jG2{#tSKp@U`3+uzg!*s z8=KBbE$t{qzc!($SG#+kd-L6SCrbYk8+h&ccW?c9XWyP;bhPvNou8cc_&axuJT`9b zox^6&J}^7*^tE@^yLH{b%vRm{nqtBHYgZ0la_=>>TjlqjboKB(3CrHfIQ4t|0foJ9 z9D8~1i%fIvHTT`yFYSg26K=x39Ez@={Xo?e*lkW8IiGKerYxV;2QB)@^47M#VN#Y! zB!ROXXm@(TT9kO=QCFddnK`&~n4DZq(hCa$>CO0do^+(gqtqbq;7I!)yT15rw}>Cv zw0!uO@@UE9GY?+Y@bM8d7W|A7>;@;2>EdBK_G9$3=ojL|w%5eW<}bbdIp$FTZR_*k zHnyUNMCwW_8eQ2f$BJg-Otr`L#kaNO$&zJPxP{lly^;7V);|zA@wo>$+RcS+xL_Eo z?%Yqys#Ho&ju`ujnRNWrkDmn1i(c;(m^;NT6Bn#nX79Hv&2iDW!EFcjeL3V(`{dGj z7uM~tdCr9S%O*`1Bx%6v!A3#q?CVr7@D@{uv&d8*e5;`hYj5Jf_93CWku$KpAan_S zMV^4w=9OHyS%7pBVq4OEq5YC?*lbp@Yu7>%Jr;f6yzYDRk?2^o?VD^q80`{G1AdQ# z&KbDzthTC$Vf<7ZD#ouyZqfLKWyl0diI>V!_p64A*f+JRYO?6`kv&6f{Yd;`CxndC zkKppc^yo7Vt*n!vmA>`Nx5#W`C|dbeTt!482CsM|D1XIs2{>@@P(zH}wX3!{UjEUyVXs2imATU z<)$_?u8agcejuMmQ~65BP=WmqH;>Op=gYw)hYROn*O+N4#b`}=rcaCIr8T6OzWw6x z7xyE@G{9%uF;FgvrN#((qSQ#PNS48>H10@vnSy26S@{$!JCbz_zr5+bk+@_ImVurr z?#V#Z_8DT@`jVNI0@S7pqg$|+o!4x(SooJu2K5^vg;5U3bm;AS7Tqc4jeV69y;rlyl*|S>4KXPjON+<7GK- ze6{V!Pq7tp=$=X#$2oyOkLd5CUKB^xi4R_gzAhenLuA?CQu347Dx$O(mRpaAg`rM} z7SzVu-J2El)sSb8=oF~DHq_~wA){wKc*Pdt-3P2A=F!k>BN5p@gE_1xwWGx3aCSI9 zEOn&N0Lnh7eaKzUiVb-ZNc(EW5*6Eys&Hg4_`OT(`&33?0umpu&?SG zPwA$(kr+DTMvAFDu0%G$MK(yNQcwWt9#F}WT=j#dkm~uE#Dz%sne`rGu-)o)%__F^If`DsX%&V?DFFHRn^H}90 zF3N)fXzv@`qy3ns8O`#q9o!@Tf!591%-ghMLh%0HcU~M{kek_OM4y6O`%_wnQP!tc zzt93$JhezWSM7g;$i3uZ0t4DOtD{g)F+mfrMh#HJLd_5v8u*AjHTnxz@kzSukYvF( zkj_~PAhj4-a8q6wOa;bze359nXT!$V15MxDJ1_W+{m1g^bXe;4?(dM9nD z!20l&I3Z(!Q$@^ul~jUvad5ZYhKun2|B+6T9)BjC5U5K9Yo@xpSheV(?dy?FgBP#4 z-6xzFyQAh?;Q(Z3Lv)(dH*}uCWB)>SGW$95CE1lPgEB{FLUv{#y(5E-p$18rk_~+l zG|$hC=36VLO<(zV?_QT)zE||Vct!ZpC$H)*g5?9p-aVjwmkT?jPO|DY>U8y$7q{&c z?t)lzUvr+hEckpNEZ)EtTZHqAPbOB*NNr1jBZ@NGI+%-aE9DmFR!R-WRtmxKt(1W4 zkMW*nxEvlY#dDqmj_*ad9x$7NJ1|yL?>kv5qOquFGQG1S-no{?Lf!*#bH-vnkQ%E9 zV=X8A&E^bKPri@6IW!goJ5C=eW1%V@bMPvkSyZZKnVRAa5p^i37S(@8yD)B=q!{*$6;Zx zVO{&9pDOH6SBYh!LF7k~U+=d0JF zAj~dAyYfeuZE)~zj9u6i(ZAZByX7rbSJmaV0m+ z#xH+Xd5aM4lhQM@oSB8g&dlgC;9%oR?9a=0+K1K$mWYPW{w#8u?rL1M^w}S3i3tZE z8v88eS0=x7%UI0tz&Rg@D|dYP;exsL;eYhlZ2vm9V;j+92^NiQvsAV>GO2HduE|i* zKvo^q8hJHsk7`YRPG3lzR6#2Znzm^nQxq#h1#ZnE2(OCsE37T{t8agMk@XBd!?)u1N*`FN;`A|8is)W;-?jJD(J0S{6b~ zdn4e;Uh!w^10^qE)6mt!BsGKltHd>F?Sq3>*`ICOWPiHOTqANeZ4pgYi_G070E~M(hHpRUVg{;g2Uj+KUh>i=4 zwWmftv-ic`I?!%y#rD5d?&{1@{*iTmedZ7Q!(ns!y|s4J+F`_VUF;p!3gAeQ9j6Bh zpDBurKKF`p%X^&`@MM;=8i+j&;vu#}wmhYo%Wi^nk27@>{Akafs%Rtg&>PXtX03(` zOYP@Gue+?A)A8o%_Mp{m_6jR#&4s5Jv~n)mQAVgQ7s?IicpC2@oI{D6q;h#JXc$I1 zkP|>-+q7!bLX2d_?dWGXxx?rMZ~kl_{rTv&jt?GO{PNnF!%Lcqae=y<>~Y;p)Bfet zWA zMgmLY49?=ai5qzIPjG%?&&LKS)CtW;2s_CY2CEV zHG?m0Lv~eg>!9)D2X*Myt%Io5`%aiu(zpV>hS`fnj^;3#&`IG|Z{v!L~#Bg9(JU4zKu6PhPTz9orLVC17w@K8~F_*C>L7Ni|O1!Sa zUoffm5)zZwEE=}f4@^?e75w5$@XFU*OQ0ss;!+LX)8|O zeD#xi_Pn-f!Cu_)v%~HqhT$%m+KBY2ea%NNA{0K+i%2XARBEFOb&E`Laj{Hm-(Cz8 z8%sO2OKN<{!;5}dU!Jxs@zrlhlk8-#rXG7DWM1A{-x#KZD{HxTL!zWxa{+N>l7>(s zg&~?j_MvA9Ag=h*P@1=raeNTao9-cBueAbDeNO0c)KSCZjK?^Qd7L5_7bOlDq7d3gtQV?PA`;H)C&o@HM; zk&3J^=GlmFWkcKXnyry~*<_F*scd_8y?msTmB;D(`p^~@raH2Od|GA;_n1tF3KxKj zo6mNgHJ2w|ySm>kV`t60_QxxS9JHPnd-jfa?Kb^*lpTX5fyTd#XqKU8&!D=y$30UQav11cJ?@p59dX0Ss3zRc&a|D9ve#J9%>2vi=8K~$buGs zMn3|Q5xC%;b7|9L$HFjOY1qMPH+t{XzZ560s4U*I_vgyhbD!&b-S*k5&9vpG=PY<2 zWJF(^V;{6*;q8Yu+O0QzK%96pIPn^9`dxbonH-J_1!r^bKu#Ezfi%H!tWF@MRjX%lW5 zxPA!Qh&5UE$BYlR-Z}H#mvdg&3p)1)ow0ndZ{X{2vP;YB%jlkSj~$a%A*?fEh|YX$ zGMz1UswMC*qO&L6lIZNQB9H^ac;oh#)Mj}gcy4YU$IuJu0-I3(G4D=&X>{evq|t9p zdb>nauAjZ_>T5R7d)mxhetO=6X1kNdgt>E1Eech9uw4AS_z=y&zTPrG=LV1rQzF^Y z!oi)Us{W2mdWAkqV)cO!dWve(T+}@_h*`1P=MS7j>d2>PsN%8Y$RXfMpG>FD!dZpX z3WhQT3%TsAeS6H(l(&~Ull=YleWR0?nJ123|K??dU6&Uvm_7QHS5H>1o3rhjYqmVH z*3A7?w0&~w>7-+Jw6N{$R&C4IpY?z^|3tX_?<>Xk%l?L0--LDR?nIZ*mvX_kPAt4h zxOqdyHLnCZzQ{T}XCa&h(Kk)~{Lzm^{mEZGKL7Iv%*t)^9@$zkZ_YL|Z)t3C{qa<-@=uQL-gfj8l2vofX%QEDZ4!>W}~Tls(5 zy%b^0f^9`6HTcQdy=_Om`M{nrmCKXIz4gjZwVvNFfBm)BY?`~#Of5S-=lMSfyz-A0L{B|=gyz!U9vp~aF3Irg-W93apdpfCGN{fszwO&SItenU>W!0?8|SXOzTc`xHkvJd5$$G-J(+7- z6GZ!8!&{F$VW!SMf;kL;1o|iDkb%8@TO>~22Mgh%yef>TS^}lpM(WAJXPkg9*V~U1 zu$rdp(`<3*cy>Cc_U9o|LiEp+eQJ<1E1aBubIh0a#|dK6$5a3MO|*I3tBY4mxL}(7 z)7^_w%_a|Lh^)`AT=L6o+de*S*_$^_85?+O#lm{io}#rFY+nz{=t`_a3+!;@1JGJD z`>%I+7S8)Dl2*Y0M-s8aJxSt`O!+j+7f_@f?(=!*)nPNnF_O>N;Df(;?XBXJWxsrS z^|p%&@14Kk;nBNZ`o3zxtZi3cxAEbHW{Z;||A{H5gWtu@X*s*s(`6l}inb@?o;|oi z{Iujf(#LMVv}XjLgJ8}?G%sKbLnXsw&}te%#J^7H)!Ff&l^SRAF-%f zQT|XLldn&cZo1_C&!0vTCZg^rvN-)uT`|4_rT^PluvAM&AA8 z!s16qx3fPVx9sS;1GkJ>+(#7j>U3?d0Rf|9j|yhuI%1t*9%7!b*ke{3p~OA!q7DF&iWY9-bY6Mir3+5)xlrsa8oO|tIk%nNsrT@u z8v>^n+p#WGhSIb6rKd!ES8DNm<*?|e<#(DJJ002+D7`g$b2~J8#)_aC#d-!SLU-BE z^V|ufa#*yHM-qcoQ?)!k&xfW#x=f@`$6BSs&Ea(yWXeBf`Z{Ki2M?iQ)TP6A!cPRo z@T3l`M9z((2qC4-^UYnTp@l!odZs^h>%HkF>et)<^rPm@(KUgh(-qY3Do|AVH2!C= z*~7M;-G@5}ryKnvCih*CXR~ikzH&+CKXo>%WF;Wv>njmon*>25sTQ5sz*D;oNVh;8 z13KsjvO2d*Y}RcScNHG+{6@PEoCvR76&RsfUZ1MJ;Tg#B7D%*BGk>z4w#f#J7izo;IHVyj&B8ljbFcf=#YJmb-UgA>pt9M%`QNMC=E% zIM@xG?c5p4=D$)eS)xM!pySg3-*US=Pdzd{XO};+{UKMpm_X2aX~zmI9hT^*3WUG4rjrzWuhtMNQ7{b?KzZAAY!JPq3SPa(-2* z9l7WtTvWMmwz#siYQ6}O{ssS5uu-@Js<((bA{%ss8b!fM9i)MI?17Bb?7qV$r_+j99l|fs=b$( z3^#TI+M$ldB+aEx>?kmPfN?)Z;vRe29iKNA;U09bDTj!^$^%_uw0EJ|U?k{}gpPd+ zXh|12c3G^O3$4eFleowJ3h04h!d*fSj}^PPYv@|QU1eHew!yeCaZX#teJ$v&IY-Yn z(czr&=-L_|J|EBpa;D;HalTq5738kSab{W_x&&>d&xCIJC(vca_ccK~t0c#6t}$6_ zd@VYkfk`tj-l3?59xfVSY-de1-g%%)8^<$Ep2dy4w=ZyJdbYd&l;o_)aB!~a}CT-s>p7NE_!a;CA@f$y+j4BwcCe}#uq6#Nta5f$gd}WiKVVyH*sxI+%LV-$y!FU&67SefKzHYp zfv^Z0!(-P2+F2nDodCR*?3xRG>=Z6a ziK}Sj%(6Oki2%XRfNoqPG*;)18eqM6FwTSrxO}L_S!0pygQb;}I1fiOQ**h7ZU&09 zc6fFc_VQt#C0fFgMq<3-;?3?;TFW#C@9NMc;(8ZaxnV2PYlOxu2kTktkrlOhWadGM z$8eM!Ys@utU0_b3f6^D~*nI-!jQIe@yhJo`aZx&=#+;#fe1Z-c!>`k$vn5Bf%);nR zoY7hL3VjYrN=>cUctdniaaW~HmDfHjabZ~z+yZElh#{kRWSoLkvZcJ011q&Uct4zx zS3)`$Drh}&qHp96oRP!EN@OMo1+B+Eo1kGkHMEPk7O4&_4#y7U5<5t5RxwqhTmGQK zXr#@vZ3_#wBJ4?d7vwoYvPL8;gENp&Xyr*bh}5e?zB*X4q8WML^wsd~3fb9%ALeoM zoZOCMu58mTvBym}4%;v@TK4ekX6x1B*zdvelYP(WfBj|kC$_%h`bS5KyY1=0^3%gj zs28Sjmt7S24xZU_P&w$-4eGgS8;Z1k`p_6I$j0hJOMGZamUv^w9zgFWGNB>~S!^&N zwF#bBPee14mcS-fOL$g1Q-@)7uIqrL#Eb&>Bf@359AawUu6aR$lZ4>pUS*|bptj6U zk333MXx4-rl+BaF4?MHt9U;EmK5opA8C7@h8Nc&=D>ug_(ySYnHhaw8JT9Dj>5OOR zY}k3-Eq4#?c~xn@oono*1^rT7njOFCvQOT);a1uU_dr&C5WWFi0v(yp@7Ki*O8T%r zjLz?D=lrG!;t#xYa2_!w9CK>{ znTWhQl6xF_!A|85eE6z~u>C+&v?!WT5$B>AX_dxTLAOE_uGRilh{AaSGv&B_p{Xn7 zA}u6reYE#*pF;M9!UcZ$R+y`@2GY=UOZH@y(Yf5*z?VJm7Vb=M{m}m9zKR>1DSVN+7a)BjOBOhy{$P%?d^LZM-Q_cJ)3#B%y_c~iPY2?nd65% zavvVK3_67)HMBOF=SU4Xay~d%j=a+NC*+Z{my|pzZ|dw09yv&RiAJWqG}YZpYV7hn z2U?Cza#W7J3U^kVDg9xqvb>OE(_WHeuR?>dGx1h-)4kDOt;4KiV{CLMt`Z;GrC0+w z_b1dSW;=1Vq35G9R5}k<3_b%|F)%acQ)c|^?snGh=zLQC6xPzi7XkWm2f7h_NAU)y zjmkO4p2McNH+(K&q~Bs~th4ChqdtuomUE6R1N7kFcYv0b!rE9 z_5nb*4E{}zy)t$cR-yXX4zy9%?k6S$%K?qCfy0`w;*sBW$}SRj@CsfbpYC<`keD7O z`77~QWi)hGMB!19;PA|IIK}>eM3p0ljdM+W3tTV{I(To?kmMRPcG$R5{LIhWV;?;j zZjaH=l`AMFiS~-#94w*L$&mYqa`?;(nA?|1=vYDQ0|l+E3i!|Dd1vkG^iFW( zllC8s%cWLr-i<}h^HlpQJYR|Pv0?Z;;DVSHc(26B73ZnZwWl-A(T$z}#oL7!0=lmQ zT~{nr(9GAU6<78uPMUbDA>)Fv8JCgvM!fR|J6>GJxL_1|j#Z%NB)J*}D-`hLti73jTWqt<owpgH8nb&WH}MvlaZ zu{#FEjgFfV7Z|&Jf7}rQC;~uaKYgPpZV)=RMh4Rm#{sJK2hSXgRpN!{9^gtCtU&C7 zDS@?JCC-oTkaNIZCLcEB2GLXe;`)kMqSD$bA2#o0ro`0_bbnB!`p(O6EJn~X7{~O) z*i6s&m~M0Jc=X#R4+=)%=LL$MK3tX~ov=`iu1^Bx^Y-I{li*%`5-5p<4q zEH{?BoT71QS+;a$S$*s>qqL^69jyIh!}h{fJ7Z*pwS9#(mQMv>%|6NHEV&2S zmj-q+{eT}o9}xQ;YuPs%^TIa#T<@G%em>?<%K0IHpu(+KuiI2w2>x13!yq|n-?BVCY8ar6J*>1T;2=lYR(bKtQ+qRYkJ_7Eu;A#5`)eqU| zSjOo3{A}D?R+*Tpu7Sa{iAk0yuN65DgRZ4nj#O!`qK~AcaZ6Yjm%eoEoN7~IhZ;`` zjzr8YaKN=1*I6U~vgKJ(`R4mQDz%X~fB0=v_U=&tBXhhC_ydpO*u6Tst~II@aJAx5oNd;c6_L|hGH zr#dvOj`wn7ohw^4-b!Ed#WIXE@N*8P!=I#q<^39EyyOtwz?yR6wJem+AMdn5v-JKS*5F+BxC-$L@9FMh`pC zbiz7gYdy(1;bSjhCD9AY%r#*_Q2+i|ery7)47(;|si^(=5q zl&}&X0IQIhd+AXl(TD_ar$D4!UMzs!mJZyDLyduZ0nfBM;=ql4Geyo{MQvwCREMTG zu!d$kjG)!I>W9Yu8Kq&Fh6J1J@SudP<={ba;HC--J#LaI`ITXVI4K>RDGuCNNq0SR zn(1b;$k8Y2Z%`b#8_}TP5|C23JjW{cM|wTe8TizYb-z)2Cr_)^maG~HNMtO zjLpXk-QD=f#fDElf>xG8b?6dte+|%$9ja7^E<>*$O&2|O6K8!SG~q2VSqpij@z&7U zUc8wu5_-7toJ$uC-3)Y5vEXr-X)T`V-<+B5cd=nToMxKt;9eb?BHkKzmFTQ|Jcb_{ ze0Yb3)gvp-mth%mi3dfoH79E5dca&&g(hM}8#3kx8S@fR;Nrq(GhwdIX5OE{x1AkL z(b`<4(JBd`BfApzSH1fw{+g|LML}0^-WWWJ>4-c~M>kLW)t8$7zsFy>ZCCa9tMPB+ zuZzS7p7?9|9kL_b1T=%AMsQk{dUB+wMZMvoIp_>Wk_w#AhUm#x4U)!ir%O+^fQg>j z9xisFv9@>XwbigW9v*HJJgP@tCHmGdGPsf@Qgu9*8?#*MyJI_e(7w)Qtm!O;zHpvi zDa`}*UD&`@<^d_|io0qaP_jG(LhK-fVfQwf9z)06IvS zxc_^siW~f1=<4E2qHlSc{c0APeW&dEd&YT&b-4aada3Bl5O7+1-nn1$nw8JGJ1Zffu@VmSDnaK$^|4zxoHitS+G z&bTl5on^>N(WB_o);U3A3vd?6lZ8&iu(38)-<8n%oD0i$#x?@FH}RlzauvrK!jItf z?h_}dIKx|j&f(e1u~)`=xwF^jT-cd?2ws2OWgy3<%27ceC4*fFhb$H{0&~4`MC`U0$$S4Wx zF|=;xdN!C*m%fYUB#{1 z*DIlkw8asCk=Y40{!nJUCh8gxMtTCiEDmG}pv*lwf z@2khqE39mkvmm{-mBe^-K_|?ZqwA2KM*0h7>}f4c>qVK)E|azrY!alvkJpjX4csqGdQ8a$#p-qANQn5*RYYQq50<`gf1#tJZ|kZAU84 z*G=#IVLdnhp=tARQ}I9Vt!i^t|Es0r>yGQ+eLj$N`ozQsrrZ~(O_C56Sny|FXXGxk zbnE1DryOcClD7Mhw)!yYHf?Piv57vAXb>P z&yMm$;e3RXRb`9v><~2w(d@AP8!-MX{+z2nBWDPI$2{?O_EhobF6!?bnGDf<@%QIw zp8R*rQ;x?`=@4#!a6X5w=D&l}oWE0KGel8SeCGHY;xE+U#QD3mi~rt^KjT{*wFps^ z1Muu4moRc1mefyuZm` zsxt6~aF*c(fJPf3?69rk?AS>K%}H1UeU{@He9XQjaGb|}#eweOj?Md#pjEc;Yv6)D zp%xAofLB|6rAF2~p!~M;fyb}RF>hn!kg+XC^9yS+G;SmC3z{}JN-58Cq5xavcKLfrmgQaqPysB_1a}` zF|BC6oisHNEK43ZLyV2S97qwxOFH@W#$4V+u!_O66BuZexGxJEw#w6 z$xF<6Cg2Z_k8!`-iU=+NbBe^4jV}s}`CM_uRi|^QYtXO`KkO|L7aKy85^{$E`&hCecL*a!=v<=Drj)!`nP4#72I~k(i)DMotu~As{sr8aOZYRm1fS{eF@D=& z#%-lI&wU%8<@7d{i;Hjn$r&MMTK$Y5N}xS6WY7-%cJGS>Vm#i+k)fwjQZF6w?PiAoExD)%7PTlBq2e=qA4SYj?A?8$0Avoc5-wfvg^X?JnkkQyjYJ zJewp3kLqKWm^qBob55IMPiwsEL)tfUPBtE& zxj@E_hjTTa^9_x=_bg<6i(|HU8{}4ZuAz2jt7Jn(EysW3j~^5CTo->u_li`9Hp8qO z1APX-BMH31v#d0;8Gl9sA9#92?3ci7WGLhsL5vbMf+ny5FHz=YS5M~U^4MjuzpE8+ za~+&Bv5i$n^rTV6Lp&Y@7I#Kk!&6l! z4eb#u^`$iuHl7w!_?>szBd9V6+z6S4nLJJPYDyCG>5Ki!)csf0;1w*VDIZzN>F(k# z_pH`yq3T`z8RWDUdtA(w9MpB1QfgA(j+C0+jZfU&uAvj0m4mhz%wZW_$@3UwpB21> zrOiA1xehS>IP?vy_9I{wkw#r|^x0;4AnBI3ASP9C$x=P zJkoe|{t9T<-|iFF4U(BN`#{qH)}#FH@phm1Hmsw-Mxp1ysrAWv|XLUR0nTh%P{B$&1j zFfBO~f=?S{LV(uT)6jeW&(!>29|hAwe+OqGvjfx^ZX6UDiZfXQCtmi})ZYqS@LQ`0 za%~x7$Df0*gP!=zBg}*@?MPj%!@~kCKgW^Nk~gTof#i+hB8r{vd_(a@MI%V|T<05- zKPX3A^2cy76#Liv4P`N^Z;&oOJlqi9zz?hI28CLI)?7S6q=GJxugVh~K9Uk>G4LS?g!pF338l5M*N3z= z1uG>X(MOIvTy${hqeiZv&K`6F3~`7Yo#-Rw+i>G)mp)p)A>&7XgZ0?|s+WSyHQs}F zNIUS~*D(*n8k{5@M`SUlXl+!~<0unFG7-)W7Y`(3EkNb`Z0_aqK);2Ri$S9KO5 zyw0j>Thz;IPXLD#VR+?{l_=kl2u^pt!B&2C{sw0Sjxry_c%_C<(tMyPi3%d!V}h+2 z<^yTX6dPMyKG4>T%JY<(+ZNz6fJZNGrt>`6%|LvR>hO%TlZX%G$is~zE+43oE2w9L zin+_tX^rIQ|E{81zJWZ3@(t^71HImfI)4tjq3#GnZ}7H~EH_^&8GdCBELd8v7)?AzgkW_7>>%UFc!`hVw^;Ykb3bzBhcQ z-*5q{{u-!n=sDEJ8=5%ZpgG(adRD*Tg1f#_@OlodFbCBeM$X|w{f3U1Lr8tY_XgED zU9Q4f{f3Ss|Dxc1Zzxc6Af8}d9BT~j;_gr2OXdx=dYW3w=g%C&Iyu%FoQZBL^c~_G z^&S1Tb9^iQoSsW(<2!MV!cD&+DePv@$+D$Rpo8 zF7>@5Q@^7x-f^k=hL2YMc^uL2=!09V4q1WE|mU>^g(oi?po2jxI*ul!e+_zY}IJ0p8MngwnJn!W^hqbD;Nz&J( zRkq2h6jv+GQm#&2D<xr_9k6Tp-0YfIGjfQD!gBf44L)_>NK2nW(|sDdotvK+N~JfDrcF-qnm~Ov`lkH zhdgjNDF)36S~jA-k}l@?$R+Qk-r(ZyO3hdmWZFMjrpY&)f3xO1-y4)pN^_VJMy%L* z!v)3@BCp0bI8u|kI$OP1rpY&4FhI*R{RVW9C~x8LHoc)o_$&Q}j+jGQjc=f?oWu=s zeMzRtH*~yN%QXE4bOO`o9&?+r>A|Dig^hU>Tz3W+sp?_oMV4uJ3(2&BQl@$QC;Dxw z0}@$zzlVuW&yk!L80zw=_GDO&Cu0dM z+B1O>2I_K+hwROsx?FmG1@EQt*w687pGvEHOHgXC{q0p&JKkf^iFxlqqrf<_lJ5oJ z4tUx%!4z?s!bWSh=Q_BvR?DZm)T}oldaqA+mJ)<}D+hNP`@%5kcsw?C!k40rT4OzS zGw^9kXN_t6D&7e+e#k7mRqq5nx|fT3;tla(cq^PYoR8|su4lX<2XAonsMv12p&jo8 z`G)gv*E>PKp%J*Kxr33U5BX!{94^3F(x}EaC^=7axQ2Ive8UC9^iJ@-LHPpZ8xHF? zbi^FuYJ5X050%=5XeY=wbi7^f1m7FfZf2fPUG#V-pwk`g1mcY|)>`!^V26FAyuHiS#k>3lb_eIQKQG4GQb(9VQdIE zg?+AJ;uOF$r#vS#=6qh#YQ{ShXWO4o5sbpnyxWJ26Ct&x71OE#@ zuK+wnhI1(fOz=)M@l$~B$@E6-kDnvo4C8dQuM55ygxxGIm(K&C*b9ird}7~d4WUzB zTp^#EBDRZmGW--rAZbI?6U8uN??d~l2$rMLxPdWLrVglR04J63=B8doX02o@F+~&n zq-5^XoRnXY;ARof#db;D<_+EOJ02u_$(AmdA!ax|k)w#2JK>!z_(q#*I zX5Syh@Gpd6+ee}Y_wV2JE6B3zehh_H1)nm;H;<- ze^p6W9sd*u|5TIzg-Zv72Y=*6;Mzd6fB(OUKlS|QOaLPhJvkU*wMX~^D}hA)#5ksT zVt(Y_#A=E9+?%N!4YG0BNiBCmptRGxUiOMIr`Z2|W$V_!l<37G(cB!p*4$ib-c@Fw zhs^;=WDs`&i#{GKutWX?3ty$ce~Q2a`*2lRnYqPI6wSpEJ4YNT6}uPPf5rQMgAIKi zxG)3#iO!^gZ$TAF7>orz(I-jLCn-OYpM+|L{rKTs-AGveJ?!uOkGnF_^&d^TaffG` zpbM{wKiw|pXw_YSt2{+Bv#??p^W#DhJr;f6yzYDRk?2^fu=KOfO6}jw2cuo0X+%e4 zs99%0j%27F{V_cY>*owwUX(TH7thme)=X62}$~f^1U||ppkl{ zmh%VNS}t*ksGw|M-KV=gdeXk?u((%D`p~}WDKkF$n;BmkNQt&EKRSIJI5{yp;5771 zoD_qqIJt|HN`vEA4%NwfyFGd@8;R6P;F2iZYflIUoz+(e_Wr8s@XPkf7e%(H`=Y)2 z%>;XdSWp_+diuja?&+(6AM_#0pEOdH^z_k(-Qzg$T*MSE?5rcsO-llW4emjoA8y{gl1vbFpp3WA=zo#UL@@Gke5Bvn+b6d4BXw zv#iv-FM7Z{H~J8*a!c%WACG@vA9`ARYG;cDAn&K8;#Ipu%(1)B`Nr9why{lz`xBqB z&8&PrAK~@JIhY%pW>3NM$ILmwy@94i53y~!ar^Dv2o~qQRY0*E)X~xBf$)9&!JlPp zr#=4O7h{Ety8QR2V6UCSpX>4GMflt%Sb-eo`Wf{7>G<3factZ#$)AVDVu9cJb0dbI ziNDiL=piG6|1SHf-GH+kSsu_PO2kWu=Q;X{;*$#1h1}l-j-dNsh7PpOYsho7&kc~6 z-tI}=#qDm+g!cR>U%Wc?oi0SkJlI#m*s9;I^qMvS-t5sdHd}Y0x0~a)V%vjsICKRl zPjT$f3&>N>nLzLYVy2s|v{+PqLwWzGleg;pYzzYJdxTr#9pFd)#`&U)`i90IUH8#B zR(gF}lIM!w(aZUU>ApjIV$kJU<<`V%0q#3~hxEp5Ha~aY!MsiLKzDrH|HJRFDH8c! z@LOOdJu;iky~rDNX3pOrtq~om)i)fk3F)K-4p#F;JM|51(~&ny`MV0hCGB_~WU~{k zYP<8$mL2**>zwoJK&!oBT8|FSH_okLJ?h~dPQGjmIWlG`zvFuOjlfX%9ZFhOAZtm* zIvfj+-T{fmSl!{igCkf(D{oZoY^=v&e#ec@H!SxZ+R8)6XTD7b`^P?nc^u$e{P&z3 z{cPSh=o$YJl`a5YykZ0438=shOcD!L9kvaz9qGI z8s|l4{CF3h>%MNYHZt`nrDCG?NW#5w;4U~ zoM`Fo!lpdyGvFn>g=y1Q&LnW9#Ms`d$WBM&!+0+0RFG2W*f@zALBrw@f$OI1B}y;pC5GKhw^iH(jd)n z+pGAe17Bz#R(PToGDLXd^K9UEj6b*4pHah&zeB3w?>+JNqx4xEP=DuY(hyY;FXY#}HMDN_eXQ!yo3)T*1hnKj6=a zm{&voe2_mwR$J%sXZR36r)O~H{CE8`c*Osr!i^}lmEeg&;NE}JSA;425Ld)~2Y8Yj z?c5qSK5401S5*yj)hE;A?>xV1^!*>^hsx)`6-=SectqfD-N5+BtdbJZ!o`QxYr;px z)SE(2NZ@V(f@iyOn69~FO@v@o26Q?`M!$%E$y>Pf9cv9r$hD?^0h-qcvs<_(9NsBV zYNfH)tq5h^ir%K~KmzoiaF*oEnmDUFzx|kcyN}Zdp8DB@s7nN}tIl~oPJ;#0)9xF1 z-&zLEaha5h?4$yQEF|lpbALT$W^o5KG6vy>@?P;Bs#Oo>Z6vQAU8OgvJTJd)rIU!l zB}2C^7xIocRi4X);geDkhK~laKNAA+Oo9ZEU_}DlCf-ZW;>k5acg%X~g&ZrMoJ$3% zIry)@I$5=nZyLP0a+SSU45OP6(E}uE22R%oJ5i5kT3>L~O>({1$W-g)LyLC-tyh3= zF@;Aljr@w2k_wMSD8!UCtKB;uMuidxU1+t*czZ+DbAP+<>qYy1ORr3u zH+k{XVtMJzc^P8j`ZqG{!D3Zo(fIRQ*8DN+;LZofz5U|w7x#~z25zh__e}e$;0UT3 zH(^DxNB0HJHkNycDzN1qDn^E*uEAD&Nast+-3@3{?(X7_8kV!~zWFQ0+Q=%sBD7i&YGd-Ragk<9P}8D~-d?S&mEi;bH_oXUeg>!-+xT7;E^s zbGryM2-uf+zD7)9Je}J*@EleoJvVmq^9}H2d$UE>Ad7pE+ibuidF_(<@0=&I1< z&_Bcd!^Po;!fV3&!r#aBjyo3LK7K*`ceUEo8e3~aLY;(L5+)^Vt8LXjtM>TX@cbrT zn^={Ul{77BYtr%LCdnnqZzZ2hDM;y;GCE~J$_pufOZhEzOzL}S&C{l({gB=@9d*tb z7i7%H_&DSD%%PdvGVMA;>g>t-|CoF4_$rF;|9@t8?@g!)MWhKgL7G&l2}MAPfFMOc zdXbJI9TfosA|N1AlqMh`@)i&XP3eS^P(w)|Ku92guMWsJ7lv5SOeY+m>h6C z;6gxhfEidO@Uy^#N);-tsg(BW;L5hjODf;5(zD9`sx_-Fth%M@<7%y{eO~QK^_tap zzt-us8LuVP=v3pknx$)gSTmw#cCEg(=GXeA)}dPV+Sb}FYEP+ssCH_d8g<&&8D8h3 zIveXm)p=C6VBL4>_N_as?ylEszTW5cb+2EnSD@a^dN=F0s{dPq+6{&@xYqE^hCeiX z*yw{ss~crD9@=<+kblsJL8pU#gF6QAYf`MqCr#W<+cZ7g?B!<5nip(7vw8Fz{%?$a z<7A81Tm0N2wPjGtkd_&(s%NA1^isM4W-hjkrpceHiPT0q=x& zkzIy$3GZ6F>z;1!bX(KyLH8crPrV!b?$#a+dxZ7u-ZQn=_+Ag+tMlHJ_wM!X*vHc6 zMBi3@PxLF%Z%Drt{r2>G*kAN7)&Jpu0Rui6uzbLafqe!>4tz1F+Mt<(t_?~Uphi(`aJgn`oZo@VWvk%J{mNWdF;cJKA8PRpb z$&uwohKyV^a@DBfqZW)>F>2GOoudwoes%PhqZ3A_jnT*WjVU)~^q9yGdVX-`!$u#j z`SAJJ55~rPH29t^qu(D#H|w}C*GR)U{c{pcPBlYTzYbi$t@<&o#H=b=hPNc-~PD9$KQNX-DH>8U zWO&HLkhLMlLoUo}Fl*lIlC!^<{rHPhU!3}K>X-NC)|)$U?$7hA^JdK3Isc{kpUw~a z>W#0KeRX<4@da-$_-w)Hh2xy4b`udlzUn~w-JZ$lr#rqdO z{-)PAiEfkf%Ze^*xh!OvV|k0^8^0CbPW;yS-N5g5f0w?Zu#m}d=TDQKl^~Y@mw{_e$Y}?-L)wYk{e&CnVzs&t*?+$Ip`#YxYSg>R5 zj?f(^cih^My0gR1!@GQUjoNi}*YnV>p_@YOyPNMGw)^_-ls)72MD5Ah>$lgoxBlKX zdwcF3wfD2Vi}!Bc`^(;adynjm+`31u>yGM2%N~9GX#1msj(&1<>CtsZw;l~U zdgJJ$V?M_!9BX*2)3M>lrXO2=Y}>ID$8H`=Kkj?H;_*huyBr^Je8%x_k8e93cKp`y zjN>_B1;YZug2LVj8yPkuYLXo=Z*8_&euEN{(O(~gU^3>{*&|Hod4Rfv7(wIxLF0Hz>^U~={@s}Q7)-D&lT={ad%iS-Jxg2tN#pNBBBQD2Z&bm_Q zO5l~?E8VY*z7ld}#g$!GBCf<;$-MgB)sL>ux%&OpJy*|Oy>m4ys!-I_s6|nmq7Ft~ ziHeKLj4l`*5FHfVI=WBv_~_};%c9pu?~Ohl9TR;o`j2b=*Q#D?dF{PxL$7^$ZOOGQ z*Y;dHd+pk_hu0ooFMK`ldhqq`*FU&^_WIrH*)c_8UX5uQ(=Dcd%&3^jF(EOFVphd$ zjtPr78*?S*W=ukiGsYeBD+isq?dGluaE#F&{Z%x1T-K}-Ee!6w!R`jjgx9;Ccxs@4PFt&JX z+1S9?*JA6%Hi>;RwqtDf*uJq7Vn2kcjB_+3&)p?FCX79{+;-~ z@gw3V#?Opj5WgaRef*aAo$=xEaq&s<&+ZhzQ{zsXJDu+IxbwlC*>}Fb^W&W(cdp-g zbl2x@#k-B}cDg(K?##O@?}pwza5wDkt-Bcsg%T=o@z>^-v}tKe(|%6dmll?GG3{nrQkvWC>#pc-=x*ii?C#?p?w;VD;r`0~ zoqMBuhx?#A+Th_05hss0LDiu!|7gleOU$bmc z#+L*kPP>cMR`j~;p7pfci|YmI`YLa|0FI^SPhTX4dAG>onHRieyFvR#%r{cS$Hq+Y zktI%yHvSO9vN8gZV3@nErmUFEM@=MI4-&x5#n`AU9rgWvMBFU zMl|P|VB?bTGtP@%T1Bx;Z!fAr3+TN?OMR^{v`;zSSBy1gh($(S(Vw~8gN){4fObYS zBW*qu%Xm8GGDKo_`XbH;%@deupjluAGi|9vvMjQ2opG*xwiOHl-(|-hE zVg&8k)7nsUwFZlEl%wx;aHVlhoyDNe!H%DRg(pGG!SdS|xPxiv*MV}SU^ z@}qbU9t^Us6w{22;#1>IGuGN(bhMb_HOnyZneS5Zy6d*60p4h?s7Jbk|LZspS!}5iQ2T=V0D$tL{@%G_dqXo{oqW#tLL=9CVEM0{WG;j%Z;VHSc)x z?km<K!h;^zT7(a_qU^LHqnjr5@Ew#B%OXTaZc+dK+ z=mWopS$l~_mKx%9zP;4l;J222PKY+vDWZiBdeFL2EaBX##w%jDWrwI_v59ub#tMFC zsg98$YU(yI&L}B_Z*}6Txl}udIVgxe?@!w2Y}aLb6Hv2&T zr%Pg#ZxgY}BH{bXVwIKWYStU@=du`T9Ya6(2K%rW9`6!u=?^~};i3iSzpoWGLyg(= zRb@+xi8qWo;!DF<)X+MK{n|I;BTYw_XQIPD;=T(-GwU@kj7QM#1@i;oUwzTfx1s1y z`IR1>2D%!4q9+&(-bCL|goc2+;GJBof(}LI-!f{5B1R8U#rhKMI8u}}ri;l&75a)z zwC3Hk1;$~@-`_(A8~4Qm+HZlrkNqCd%c2HxwUsK}_(crWFF@6G^+jEMqbQ>9VqfKX zn6g0iuc+fCu1gawEF;BWcr_SVm|~eIs&L=IKCMJmpDtp7Wit1V=SKU4Q1+@E~F0?R3(VP^+=?x)JF>T+Ma=^IF1+p)XKRv{~(ALs>| zfG(hezCwJ1U0ko*Ik$oJW;OR| zhPe;>{Jnf;K2=oJ!y?2dw0%>LFHsM#abIp6oC(_oLS<27)kHGix_+85P%7s?TOu+XF zg+2h+(V-q5Q3U@c3KGr}|G&ewM0FKFFtL1b)T}=w)yidGh*K%E!tNd@JQ& zE<)&R(y)z^jO}o#a_?+e)YZ4XSTC&F{cAaGtVQ!F(M%;IRk$#BBPs z$37_PDW9?p3i->Z>`ES#y~q#6mptUJL%wZ~tx|PTP&O>B&=#2eA$6{>r!hvwZ42&Zm64 z|5h%>2cEH_(gT!FmCd7{RG(KqwJJyM7|^3rD2vhuy7Is2AN0eom0t3U1M$DrIeD`D zPnsv&p7FC9S65(sP5)MYmMW7PKj!JFys~<959RQTT^Mh8eQ5KUZy(XGKnMD?8r$Ok z=jkyIPZ`%Je)urfP~!tnd#JH=K0TP%hw}8A$3JB}rp9D>*Zr9{JX5?-yz|gJU6SY5 z=TXKfYAm7n?$N#fq>OJoV-UtQp0NjG>b&~?dupEl_v!yepZ}wNdDrFD?>|#!0W>!c z{ioxKci5lrp+C~rU;a~n=G%YESYn>exBqv2n>YUbpXfjJ<`VQMp@{%%wywxMDq$dl?9svmm#>W8AZ zm2m?8``=?b%ka6k5Z_rwpWY(o{b>vH?2@;Qg5kA_rBtWC7L-qO+_^lfZ={Nz+J19k zuFa@nY{`jH@gLRyJn2L$njzY&<{Tp&T{hAzXFN9_d5+<`DShwJ zp&tE?KaX#({QN)lw(|d#PR<=Gcx(^b9y_6YeDt>(2dMNsTcG^&Kl4{UthxvO``_|q zEZ`YiC|jiRXB^`3yOq6AY23r2{Bh;?D}P^&8GaKht@w-{d%}H{Ey#oC{M^3vXCJ}t zse98ezElHApAU1Ga8aPc({ec-3)J-vE!8;6`^VF4-Iyzv2n$cP*I0NqE-QMeepQT6 z;YS7UUjnaIbrrUL?AcVxTV1-mC0g$+PC;{vaWNGdm4t1kB{NYK_;rIKXs;NwG{BxtQ1JyjvNe4*9={i zm_PC-onxN0=OT5p+!8afgh{ac<#jARg-Y44yP!& z)khTI5^ocDj<8ad^89fvthTCB!+ceG!_#_PN6xCes%_v8S9tyvb3J!emm39CTd6-) zB1+;pUp2ZQH`O-Yqq>LDDu1rgd}yOT|5VkK)cpC!b(D#=)zqETm7YI!T%DS?uMXz> z^W^g9zLDEJsyv=0-^qWbd9qX2Y1~ioO34nw=RG0sN*zhd&pRc!xuV_Azy?;->oJ zyxg9co2zOORcFeH%&B5)I{V(1R*gr;DhO41`1(+4#1^?{pL6rr;iZOEEh^p1ga5i# z1Dzkta>5EQkzVh$}hH_8>`Ba1NMmgH&x~6$-*P}bTm~2I7Uxbnns~KYjmWC5)HI25Qg<5 z_OnC?vx&GnxlC@9 zq0%i)t(?|a>!Tgf&S>Yfcr8{h)r1IZ07^j8WOBW3)B8 z81Eb7j8BcZ#t+6;V>jOlxMbWg5{+!W5AAO$!GyRL6~3&HULmu>9~C>>3fKzSirHSW zRj>uxs@m$>>f4&wn%UaeCfh!@ZMJQ*{bJkAJfWijmVgogr2+y2ssz*y7#OGpS_Az8 ziwBkstPofuuzld*z~Pn8KhhqbdS>L9ruyAN^`~lWqt+X#^-d8c&WJ0*!IuF|=`V}R zDzcho0g&+AvIwJ@qM z6S1Aqm3O1Z8`F&W#s*`XvBx-UTsCeR_l(ChY%zXl>x~K_6*g4JsE|dig{`2?-&Vr* zvdw0zOs(tLg8!`bkG3tg?bQ09?a1G1-7mM+#q!m97`2wvT2kxh)EX%;ADc6c zG*4hUj|1KgkUTw0)jo^J%J!BUPh&(^HWHqUU9?l2nH ze3LNe?zah_CUm~L?anWE*WX=tcg5Z1cjw*x;BJq*-EOCr_^sgY*2CH&*11Dvsh_0QguWEcX)<5m@8*SdrE!D6rm90QlY zJ@2`HUu#i+-}mlmCpjPdU;ebuwGeHVHe35bn?scIGwmB~iMCW*rY+aL6~r&K@3a-# zN^O<4TFen&YHPIbwYAzhZN2t`m@DRK+qCW4FWL@mr?yMX7hh?i+HP%+wpSwpp-tmg zu@`C6c}{mqT^by_jBHFQLuQW@=w*8}u4_O}&<0TQ8^=(u?YT^vqrQ zToJ0z)8`ZK-pwz$F433j%fw##*|%b!{vA=`{rXDrtGL>M6 z;-N?pE|IFAM%L1VTMy@ZldV^Uqp3Aq9-rwS0o>< zmsWI#LyzLkF<)80n58@Q2l8cET9%Pz^@nT4>Bh&#CwhjSiT0gmd@ei5&c+O5rkyP=O zO&8f!@+D)meGl0a9X(CO0 z8Lt=%<;7t|nJLny77t8?(AenUfHkQ&0m8S!wk*><<&c!@vI(SSv znpOM`o`Cs4@dDkcg+g<3fkqW3dc}|Q{ZM}|=y^gG^MXDo_+FWiXi|;($tqr`FKx)G zUeHH{MBB=`oD&Ou-3!lKrb_QMCjCA%$P0n4lEI)Q=?|c-z?+l>p2*gq1L@R*Z?+20 zTf$4Boj^CzsSn?w5%OKmUk>d729Zvky(E-2 zQxNc6s(S;m0=mo#u@brgC?2nZ?gEN`w1sqgq0?53Pd(7Sgkq6A&p_05&jI|^YjE$< zpbXnsF7=Il=C267C{*zVsP6@9eZ1hk51}E$3i@3rd}lt0;LC18JLiQUf9)nfW|W+2 znO=y0aSchjWc4$Y9nUxrdC&`ZA+|&PyioG3SMoy1xQ@)})I-TQy6`W|V;czSuOm-7 zvZMBO9PrITm`6WX@+n_TAT2$!p7p*CHb}rDI9*Dir)c{_4&ix(=#6Bpz(BXmS zI_j(gfqd#e{VQc(9)JlabB2ce2b zs($MJs@$ZB!_Y&yP=^;xI1(pr6Twy7yT?TVfCR zUIDlSR9UWqDAF%O(Gvm0k$0Va|gGSQdtwUWjO@1^9A4@@pUi z%wrK)9Ny0LLWk!@5ij&)Xi+b)MZ!Q%6qK$a{_6quN$_0O1Es%=;-Cci!*ewsKtbs< z<0UT?AB|F=H0iO>GF~XY8fCq}z6qn87fKHr81)<}? z1oC%6CxR)YzYLuUJ|uH)4*KLS8X^SpaYamAeQGv2=r^Pl=Pa= zWnej&4ZZ~{Nq-%>3asW{s*Towb)>5{To35~#vHH#&}WqmF}8v2obQJI0(Jn(YN$T2 zi}Va=DA)}spRorV;#$>iO12a}2lQp;YI*F8ag6QbUSC&?!ZdJ52<@*SvZvx`9S zK()su(tANKgBzUxJM^X(>O20-1M)&bV{?H>FqcRecYy{HKqC3{f!+g3?o@jzIaB$4 z2A+UFKsoRfyx{zP&>Ua_cyA$J1!Wg4+)shIJi@{~EdHbqh8FX}SO6^!=)V#Sawr>YTRU~;4vl5zxEy%2k#eqKoWMj-qSEYA5mp=G^LK20DzR*;lE zu!a|EgdW)53%M6M*b6m!4;%)DbN+rID$~Xa@;6kg1e8M_zzsd+g&OrgBmJ2{Iy}fR zy^tqyKc9maq;qblsQGqifv&yzwAJE12c-FB=$u#h-P1t>YlX_1He1`#Z9?VXTA^Ca zQ2AOwtx&zDtzD>Ixn1|(mHOBg*cNmcw!qfTHfqSQP@}Ska$xv^K6PxN;@#e(S@-B2 z5Zba&xj#3D_vzE5R;Zz};R?=P(1#4hQq~!xOM6S}Q!~{1TCGrDZw~MF4sBU3RPNTq;sDxp$9r2tCU zr%kQU0yW=$w|85L7(fvV)(oxIwpM7NnxWMpg=_AvD(Bl4yxaR=OPq_L2Yki+9=#8W zYPxfLpK_sQ*l2FMOHCDcd)#9Z-_Z-?ZNCgQ|J9^zN-2R4%2`jDek-)hMI3ERAXg zmhn?t)(HdJ)hnmAe3pLPzey#vyrF}3B_vO)D%JlFbnZRIVM*i*ca4;ApX+nyF> z<)l4>)arA!s#8`@-qS^`#;MgeYPCzPOtq?5R!&h@Oi@=%QCCda<5xjlt5#`hRj~r+ z4N|Lb)atTYnQGOd0w<4Ct2#E$8^;P=$@fFn3FdW(Pw>))iFbIfc%B%{Z>=`qoA5{I z(TkP$DR`+@l+?Z#Ha$QT!iOn|ef8&ARY_}U@e(?tm^DlkwcZkF0?%I&u}MW|l?N!$ z7`fY$G4sIOZH+fT$K-D7`O=Mi+ZIuUa(VONOHqtTyxTsaCA!JGU4ZkS=57}fC8afY zyD;zcmd)KRBKpggx!XlW54l#fW;{J!d@3f2(PE?+MYPx^>WRAIbrzd=i=*R6|4qV3>CH6YDLP2(C&X;HOaF#TukE3iDD}2VVv2WtKSEcL3h$diYZ(-gfn`1&YQ&b zctSyAKy*J$L$mv~gkt>-@E; z{kOfp9~sK|W8usw4_79U<7jeHrS#rY)o?QRQzh@jv7y}SBTvnTan2OB)!iquJ()Zd z=UTzzA)M#k`};LDIsWgsQPM3La!ewn6YbSn zv_+ENWnIg|VQe<7@A)&wP|APz$CEzcE7Wg=`w6vO)O(UvjqN_XUHt~BZINDy)O^Z% z7K;>Lut+hNMT)OjBr%K?tXI*iq}a?N85!^3?z>qeQhS*7aTX~~ut;%=MH0bLG2II+ zQbp}pUtuIB8Bs;Ej%73=MIwtNK9kJaAs(HuDaQ~smhvOX&r}d08_)U>HdvR3kLJU=fL4fgF|8QuQp7Q&R+b2c)T+@A=y{EjYqhjmtm|s^Sr5{NvYw{h zW1UPaKo4)$C6PaJGrlvfu#Pg)S!dz{OXE3Su;c-35qPfTM4*3!sL6IwTKZ?Pm1P^tc9vhn zFN)`~3ehWxoBS#Au!G+R!bf3Wh=NFGCx&wEI`t4`hkiQoLs53*#Ni)^vL6na{E;Z@ z8HB61@YP+o*U<5k#*3mK44?Rsz4^ekBTWfXK!nq65*`5@O7Z<8LxH_e7N$XA?sf3CQq0 zabF}M#df6F$#>c7Ad3x&<_05&tz>JyE8PM4>m+*}>G|US#MI|% z^R)TeSK0z?p|(i-T3ej|dB9)K0<>SX1KJ^;<{i_*w3FItEkZl1o!2gEm$WNd6ki#L z;b~W_7N_0O61027|C5RRKOpL#O2j{%NPjl*{inqCb9f@goCqFA`x3=3OcdXr2!Bbv zlwMjdtC!a+=r%o2e^sxnSJkT%@2{oL^+xx3YPitz1nnDdj9*3gReWDx$(Id??Calq zV){Guop~{OeJ`@RU;kA_=6TY1=UzWP0C! z(?r20^dDqgdBRyfml{Mgi12ytJFrns7de$4+Lq7244(nQ&=l} zC1^o#@8EA6+-Wo?cu$dbjr#|UXt=1zqM#;4PX;v!YSMUq(1@Vvey4+{`d4Y#jCISP z5y2Pz=QfOJloNchQBE=6;?@$rB_5Vs6g;)$vXX0qn!G%>%qL~Xlp9}eZn;I}mQ`3( zVOfQXd=Ft!!wn5L1RM)09XPnszy^0Jhp93&d{eE1N>^FYa07cAsB;8+n{)Z!A-OVM}i*)yPIgiZm4Hzurt`L_~c!J7BtGq zzche<-o;yDZ|-?#H=5J9fBvQ6BJZO3m$w8@4W3Gw`{$v-Mem|ors1N0g8ZBObtg}o zX|$VaIhF236Rw?~hoI@6WkC>2@VB()RMnztX`-?CQqz2kCrza*4zNHUHaOy4a*z2^ z(rKy;wDTOccQ&3DyhrUT4twPXF3o9hC#ZCzIjW|?Q>p8;h7sO#8Xsu*rn;(OL|!gI zZ5mt*TF~H50cXBdiz>6XmB^uq;*;WvYU2i!Ab4tCyQ(93M-`t{ zYb!3OJR81AuDQrN?|)1G*#`gI_tyF^j^}Yck2ap4%XP*1sX+^rR0cIc?jsr>fJ1-Q zN%4-dPH((Z^(FctM?7--XX~oIsrsF#Z%$PlfIoXUH&XIgobZ{eP}D%VHrAzjs#{pvDicxmH^R>ueJo@ ztM$TS^k%8b7-0aGV-QPCMh*+G9V=LB$pQ*pS04LE@-5h8+pR-)V}s z6OJu8gOAe^OA&>Yx`t)wfMrPIKItsOh;=@});tr>@da}*lwy?@iv6H+cg#R^Dd*bG(cf-^c`+$Zr^>$W(qw$K?6ScUn;_ z?Fzh)x?;6fA0K3m)>doFT-vv^_F}EFQ(`@qY6i2QKi9q$8@1Ki24-<@(tZ-hc>=vl zoYMAbN5na-(J>K)MG6zqSf!KV8kXs_xQ=zYEIbxTBw&-`#C`129g&1>N)XA|r+Y#@ zn|26gqaFw+b}B_Yz*eP-huEt$k%G;7AzYfNnc@%JL3{u;2TVM}UKJJ3v047&1$L{r z$iZIOgo(|nECn{Jsx+`$)ulzRqX+S%wzdA2^w&GDy#e9lIu*sb|mf4eZ)M*%E7Z zRKA5@c3O7SBlI(}D|2Zs%I?gjiI%;vY}aLPtlJIQ2MZS``|5Y~Bsq{L%62(SJtdaI zv3hPfk|(~;u%4Zj_dv@q6d(cxv0;XfL-L9gX+o zFUBBakUYekl_~Nt&y7EpC)874c~bd+^0YC_m@UJNImR4$#+Yl&m1m8W#`p4^!P}?u z5_5Yt$jiniW0SmUY%zY8QOtSSCa)X67&~Q*5o(0WTg-vkBV&zy#vyszIAR=;_l)Dl zae3c3X++2*-@{tj5#LG0pX*i|ZcxXJ7=?06; zFtUwoyyrq9$h;|{K#X})27|WdO}!}a2gSh&^QQ4JILDMLcamDFxrDPe#@2h!# z$R>*GfzVgMLa+v`1JR^k2RFbi_8;;dUIy>1j0Y3JWH1+;WbA($M367PI&RwV;{450 zqBy8*_Tnq!Pw4R_MO*WZc#GdKZx1?vx52xhC+KC``SR*R(bshH3sg?=EorO4_oVG1 zkA2`U`$su;7J3d`;8+y-#(>|=6sdzEW-nP9TGn*R3eZ3hObp;nXj|xD=n(J$m}{1j z3&3LV4M;J=WU3ja)d!!Oy|fT83(N*zfH~kxu*STjeGk@xbznXC0c-#p%@l1D_z`Rd zKY{Jw7qA2D1iL_}`B2*p_JF-$pXp>yxKsO;{C^{#1JHxeL(s#}qg-Mw&bpd5GwR0Newr(Vwt z)8T{O05k-RKw}UDI)isW7ckfCr7s5S$$JymQfGY&*KcKe8{6BVdvc!W)Js3XJx`f; z^wXqA0NPPM$GMbWkAlX6dz^QlbL^z0uq1|L z)`QlEHh?ySHs&6|pebk$T7XudHE0Xo2I_ZJyMS&0nKzJm<2}#^^aBIHATR_BH6I$o z!ALL~d;rFRabN;*U;=FKh!3;B^z(X^P zv0RXOT{JP1L{ol&r@0xA{$OloUYBKG8Kofkw=BMLlZg#8M&38glv)Z_{^38f~X)FhRfgi@1G zN`HmY^C?4690aoeDp&~CfOX&m>9?Q{`OW7H^9rT7LMg6LiYt^Nlu}%w6rq$Nl~SaN zVxqSBNCcaYwC}(QuoA2S$>t-yBq#++gR-DJr~vlx=EVD87&yW8o$<%~(c%91T_uT) z6-Pfh(T`5_qZ9q;L^nE#bCp0JI?;ztbfD6IPV}D>o##a7DLv;z&pFX?PV}1-{pLi! zIprMTk0;|V=Yjd)E8&lRa-yG{=qD%o$%%e)qFbEk7ALyJiEeSCTb$??C%VOnZgHYp zoah!Oy2XiZaiUwC=oTk^-br6}(vMXicJj>2UylQKft`I9NCzhJCxHfvf>%Ie5Dc1v z=AZ>=1zLl)pbzK=27n=8E!YaSQ}32=$px2OaLEOiTyV(+hg@*T1&3U4$OVU7aL5IR zTyV$*hg@*T1&3U4$OVU7aKi;RTyVn$H(YST1vgx9!v!~7aKi;RTyVn$H(YST1vgx9 z!v!~7aKi;RTyVn$H(YST1vgx9!vzOiaKHrzTyVez2V8K#1qWPkzy$|faKHrzTyVez z2V8K#1qWPkfM2c!>Q~D6Jw`a-f&(r%;DQ4#?D)IXpfq=858L9xwz#k@E^LcS`vW`! zFSs+cLJzypw=Q&~3;pOqAGy#+F7%NLeZ;Tm0XT?0a-oA<=n4}MLh@aV zB&__(eId}-%*5hnilgAMnT_Snlyl4s`6ZYK=7X=y3@l_O7BUm-l!=AO#ByX}H8Qap znfh%rLyrS@K??hsW+ql36DyF3705(uW}-DS(VCfP#7s0|CgYo`tZRV!pdt7IEC;K= zUT~UT8BJ+?!ONf=cm-4h0iYEaL#f(=cA!1z0Oo=vU@2Gz$W!|k90A8b7&r;80BWFJ z12NzUcnY3VgIZt#SP9^a&NX@}a8vq1pa}2-9YH7X4(JNHgC3w4=ndem(H{&1gTW8r z0Cj6g?d{awPVMc~-cIf9)ZR|*?bO~b4wxt5fIl3t!vQ-Su)_g69I(RyI~=gX0XrP9 z!vQ-Su)_g69I(RyI~=gX0XrP9!vQ-Su)_g69I(RyI~=gX0XrP9!vQ-Su)_g69I(Ry zI~=gX0XrP9!vQ-Su)_g69I(RyI~=gX0XrP9!vQ-Su)_g69I(RyI~=gX0XrP9!vQ-S zu)_g69I(RyJ059eEMPM;3cC@FkC}vznIuk|_e882g&&zD>qA?E0lc^VxtWANn1nx= zgg=;sKbV9+n1nx=gnfy|Z%e{wOTu4E!oEafU!w7ilCUw+_(w_DnP~lcGYVT1t#ht^ zlI>e;-(x!in$7+bwx43*bTbN{B?*5e311}%KP3qtB?k{FNsn-%E86{u+>HH@T&k>U*fsfZa#zpg;Pu0X%8K)~U!JIMEGR zMW)erF#@?p)5kGRh(~KAqBRoH`f+IeIK~O_X#O~~NTN7~Wj#&{pWytHj1x|o4@5Y> z<{M!;uncinhB)qcUtHjti{>Q}$?vpXGH(kP=hMGLI>-cBARGM7`SeXe-xSY44*gyL z-HekKXlXMBt(AzCiNngoNpy;A1e$?2z?-DK3+)Ns1N29%P#jh$4l5Lg6^g?O#bJfw zutITIp*U?DdSJU5gKm#Ox5uE{W6pFujpc_E8uzxYwzZk4;99B0Ds~d+cjKT88;g84TkH_PW z$K#L38y|qNU>umheJ6n_U^@3-1=e!S2Cm)A@vUrc2m49?4V>bM00k?=Gm+>M0CA=NH=jvo^J5Q%mp(QYLAG!mVL zL_3gJH`3`wD&vq!H&W?F3f)MZ8>w?6b#82IEH*Y48yky_jm5^sB8hG!(Tya!kwiC= z=tknwkhnA?E)9uGL*n9)xHu%vjl{W;I5!gKM&jH^oEwRABXMpd&W*&mkvKOJ=SJe( zNSqs~N<*sBkg7DKDh;VhL#on{syL*|jYOp(O>sz58j_TTB)O3uH_{V_^th3lIHV>{ zziq~1^JB63u}F^_$%#X9nE4Mf**1COT>=dh1tn>PQlK<=h5g1L7&HaVK?~3d@Jj_q zi5p39BMELK!HpESk%Ba&APp%;k0l#Et^iOhSRE#JQf#Ujn=f$n(4G)I4!k-mfAo| zZJ?z#&{7*{m2_GqomNSwRnlpda9SlC?xw@tbhw)ichli+I^0c%yWwy)9nOZs*>pIY z4oAb`W;omohnpMV<_5UA0ZxX)!EiVh4#&dbSU9y$r}pX8KAqa9Q~PvkpHA)5seL%L z52yCw)I6OUhg0Kl#xrjri|s)N@HXfPz6GnnVW4FAEc6`s-E<(c4&>E=ygHCq2lDDb zULDA*1KD&Sn+{~tfowXEO$RdQKn5MipaU6nAcGEM(18p(kUNB{4&=sx+&GXM2Xf;;RvgHR16gq(D-LAEfvh-?6$i57 zKvo>ciUV13AS(`J#eobsXnzOo?;yH9>2}VdpU)OgjFnkE*M&oe|KyQQ>*6%NrS%HXjlbF9c8j(+IIJ zBgDdt5DPOxEX)Y8u#D!bXffa>X5v=2SL(m8`20o>okr{mp<=O^Sqh@qB^5@jgk)}q^QBW)b zqv*nnq6;&ME{t3{nU_!q+4PR2iT5I(e~qRK6YWtWYJX@+W?_{ArNRGZg#9+xbOv2O zH-MaB1s`GsA0p3xjlKu5uSVb#$afN$0@P@H1L?UVa^n zG!Z;xJDv0_kWJj?59m`ySE?5vX99lm`+=$Ak;f0*CL17Ew2dT9uL7!p*8qN^{+MeG zaE^1C#l;A6Ab5mV+W@oy>~mEHS7mTj23KWpRR&jOa8(9RuKJo;o+}4H85yB{vphMJ zHM8>Pf>zB!t7f4+v&b=%95cyLjX$%{o>`upqoA=M1!RJ}JC!uE(4JXn&nz@#78)`O z4Vi^@%tAY6QIbsNOju1c00?3n(gZ)W86{EUk#6Qy{NAhhyjLl$ozkXZE9{h1`L$O? z6z9hP?kw)Wi9~z~JJ-87_K0)c&~%UqvOqR?Y@Wl9y^0@u6+iZ>$T6?t$6m#Uy($Zs z$;8aAVQ=ixX5Pn#y~_AUaU%_1_A0ToD0J6p*^KQsKx>Y*;aFSJ-@^Cp0Ny3NCwLF^ z2XL3RNR#iA?@%U5&$Hp|{QR*ueAH!1H<{#~k3Hd5xB}d&X#)&}>f&r!hv$LY~s_ zNw4CQUNuS}ktG>fz631=eHmIBS_XfwJjd1eYy$ggj5Z0X#%WWaA2Zhb)Z;r|^^DtA zaqfQb8#vCflYFb@6z83WhST=dkd$Lcgp!6dq#+GSIEEw~qxH|x!gp!OR9Z2OR^-VC zU;S!AT3cu*^MR)o#`0CWefV+v;X+aK7#uwYM~~^1*xv$9uze011!6%8$fTwvK`Brg zOaPO>6!0-$B|8pIu^mo%T2Mkevg|;XGmvEmvYSpR?Ud4vyk;P;kC0a6H2k zvgbhFR3A>K)OJdJ2^mxUIi1qmkufJS=0v7c|4v7y9LQ5D@?=Mzt{^)OYGbE1c4}is zJ{-t~1KDsO8xF?a8OVkMnRrAE?Z|}E3+c4IgEn^1z7E<}>4|jOR_Tg#+Vm0a`G~fB zL_0cZLkI2WpzR#g+D^MUX)_14x6@7z+Q`8yzskf+s#3cepgw2_g3JUsk^ndEiZ9q- z4pxCZoVypg4;saBqN_an_J>mm%#td}3@QKIo}D04i2`S^|A(2tTQ~m1WSl(p_9rgm zgnJ3ZW}FheFDrro5KLMtXj`c2q3xlnmv)49hIZxJZlE`E@jfy#%)G;!ivDs8*L=wS zM{s%^`{Rik6Oq;0f%bq_&^{+E1k3`n!54sNvqrR8o6GSfU@2Gzs4KA_CoObW`<3Gd zz#(viv|}I)oCH?@Pl2>+AO<`EPr-9+Ur{rGmQA2#6KL7HwCr8tLr&fh^e0B-q?PaT z_MgALlI=BYuLInRxRH~%k&_lrpr!BfR*65YouH?Z<^~z0Jt6I>@HYz4nni#gP`$RS znZO$+{)P<%f>%KmKvcze4b%j+L0wP}GyomBwi9>KHv>EfE1 zjpU?zY(hNScd#&a`brt}u+rthY%9Ip7TOhktn~BywDK@8g8dIwi<{ZBlpBtx!|`-D zo({*|aNG@--Eh}UUs8J3O<&4{v*{lFdW-#sq-Q|Ukske6g7NN4;AJqLk<9CpS^Md>Sc`iCEV!;jk8=@&&k8srxJAeQZTa0kCP(PZ8r z#~tL7$`h0f&VNi#`~x{Qgdcsx4-VKR&!}=Wi$k_%+pATQ=@&|~B-1aP=*T4cMN#^N zpBw;vpX-K!5o~)kje|Z>ls@4{AMm3O_|XUa=mUQA0YCbHAAP_N4%(4*KV;nxS@-*g zW>Qwmj_mruQ9E+$2UqR-_xOwJp!mSZuAf&+k#>u;d+a}iW&r#rWY!Ov^)qyQsS@~I zFM*dq8NMFKlW*kK54rV2ZvBv3KjhXAx%ESC{g7Kf+4X3P-o!RI?#Lk+*_2$GFqu9R2b`12ECw7`F;@Q8$7&n8_hXLxt z-C%r**3h=lu24Kq?Omc zBxLUa9J-I}CBq>VLrp^Nl99V)^!5Yf?g4W5fSM&yvm|PkM9uC~qal(w0t z2KT5z5;eF-4HD@+Nz@?8Q-f4!26of{^*zrZfAQqec5lp>Y>no3_wFcsr!dM+VU(T1C_6>(3z!q3_Xo_2V3eK0C_9Bwb_yeD zHzVm3M$svZqTP(5)jSfnCo-s_fO!$X{Lw$}+z>aTLWzlmGOw}-F&{NzaoA;yvY&vkIiyi01@C z-vr$_J{UR#4CS2R-~+bDlRk%d&zE2xm=C@J|6j*}@Eo-70o>B?9yD5#SyOIp1K7wc zjZNT3uo?UW{@v4$ycp4MTyqf6BebKO%ghX7fT7wc5Dv}&_@rF`k>E183ZlVva0BFj z(((Vlcu}bLsS3V;dZI#n9cd5sJWV+Z^~Q`s^}k|9MTi*{A!bxWzt6dL&hf^LLWyaH z>MCyZ2kFm%$t)HLG~j)b5=zhDNeceMU-6?*qasor04jmXpem>iYJggx4tO2Z2Ms}E z@?*x0(G)ZXEkG;K8ZZmT`0t;(gc3guH3k6hQe0w*B!x9WE4z71s<3B7)r9T#fG5hHkA7y_no-CF2Yu${W+Mcdri z;w)@2UOHACM(dp#ZkK8%(>`u11EO+5knXT;6x75onoH*B#L zJzI#UwR6CiU>=wczM_xa265mn=lIfchwv9J*P?%I2@m0tl7zvS2lSm zFCv?~vdJr(yt2tlc?#L&l}%pRL~OkC=53-YvtHgcL&SR~U)x6emNrA=33CaWE(CiU zLNq)?+X;4oKfp8a!VJ+Z<`Ufp6aa-l5wMx`Z6E?fnoH=(A@t%9dRYij-4LR>Aw+dU zi0XzIJ;8fm5ZC~A2%E7B>;}guO9>fmK9bidJEKQ=lNx4$C(OC=2gQlwm&EhwMC9ZX zXbAP1#rABXB3A&>93v4BJu#l*E2`X*$t|7SQpoKAxji7a2jupE+>*)d0l6iU+XHe- zC%0sBOD4Bua!V$+WO92zZppkg=q=YHbzfc%jNx|$ZX-KVagg^Hm<1wQAyY%}EIZ>x zz0ACoYdjNi^E54$=S#)JdB)7@nP?=rU7~!q7$ZJpbn@7|LCfBtWuqDQ#c*{vu@DDu zGZx`pM%DjfJTZ{lls=i~Mz3=X?@ltlF3vb%C-15i7b)bOYVHte$jl33CX(o;5Al`q zM6YU_@v<&`oY;?S33%$qJ!0h`^QaunxgSzCjZ!G-31usls3`LUMFB>$r%1m{=`Jxc zbx^vyj6|@Z%_T=^#>ba;QrQ?mmk86cwgf zf;#z7r=s+RVwA5LJ>nPY_7Zh#M%{{1$}!Zh1NAFRNn6OyIqC2*Ob*IPqsP2Xj~PUL z3sPSTC9exluk$uZK}I}k&SO#YvS;37N1k%^;#oj%sG9FM77kBmhR7_Qm(Jz;jbGvU z9AJLu5srK3cSP{^ND*53fj9;CBZ!=xF%MD02ekGDT0553J|n-Tb#I#&Xu%7#U?lQU z3GVhr1N9>ow+4P6KrRl#QLn6=MLtT<0^#uBJazkl7U)LZKBI2uX}@^tmO*Pwp*7Ca z8eOQuK z#_btNHKEU_SUJxKy>cP>1sWB5K-&7jhXa)Cyt$7SyTNq_xo!`w62-M=x!(p#s-!rH zJT7wIi{x_wj;Fx`2PL$_11Ig>&)iHIdXSnpue*Z<#++pYurg{l7i+45eh=r=D&!>&yZt3xVkh~Hynh22jSmA+TJ^dw-`0L zr(_uJ-ui#-oq3!U#kI$)tLhGS7MNiGXF!%=6;T0MlqgX_L}gWQUx(bIm!Qxl0Ic}wXBCFI>o-bR_{6-k`}+UFhG zLb!ShG219rzTQ3}-wkjv^Kga{G0)*cT4FuiyMgDm0cpk8pYc5DW%#y}Ydg94!i`@3xejvrdtMxdza=$Lh8C@~=x_f0TP0qu+kdj~N@_8-y zsefg*Y-IQ@T>AlBt$ZB5hdeuA#~aOr_r=LWW+lVdp=-l?!?__V$dO}>S-m+sTpDiT zxi^ebawWK1+9EuH{S)Q$)tNX}rXv9NrSn<6UL= zZYm4^6#kNamhBheBSwDVC^=X5Jd*_JGrFEmL zCfBY%<$;8T=yfS?EA^#qY42EaWm@_`%E;7<^+Q^c$~m}T<9_n;1XV_XD1r_j9sF0N z=WY58T-2UPtA2wbGFdY6N%=$h8_8OaD@(${n-YJnFVlmCcUd7rOs21fOQTfMZYupU znZgxyPx0=|b77yn)2}?qm0J5w7(OF$d^I$IZV#^sSJr(LHkPxKZ`xNglpUL%oR z;iKXF+?d4Ax1{0TJ_u*Xs3yE$`k}gVeJ>(gzwGGWh4`rN;rCh$-`ha^=4mQHcra3c#r-G0m*a)o@6`{4FA~5I0*4|tII#dV6 zLpVh75ITc|@F*oMU^~JH+EZ7>dFqCC`xOupUSsQlcKZ#!jn<*v?qxRcU2`<}>f5*` zIyzR@{EO`ba1cJ{d!f#RroW)5n*oZ1Fi?>YPF5s@Qv$(27_1lw!xRJIbj3gzp%@4w z6$1eT1TYXlKmY>)^m8x}&QT16bHPBk8HBW7gGoJFF%ZTm`oUO5Ke$ZM53W%3gK>&} zFkaCQCMx>DBt<`%tmp?*6#d|8ML+mH=m+uL~#$w75AW>;vTeD+=C8^d(csF4?2mZ5U>#h z@t}ty9`pwBU^S=+f_2beu?_|(*1<`Nbudt|4hAXK!D)(ha5`8AdxDXQa_}8RIk;F+ z4#p_T!DWhaaJix!Oi+}A>56i2x1t<8=r8e?1l9ghe`!#ohzGM2@!-#jc<`_y9z3Fm z2ahV^!Q+Z}Fjo-|o>0Vt1&Vm^v?3lXRK$Ztig>VC5f7Fq;=yu7JXoQK2P+lvfYodu z9=zxmfq3u|hzF}cMG&lmw-oE(ZN)lRuUH2g6zgE4VjXN!tb@&pb-?^XFjf0!bM2qa zwSTtN{@IrP*^$25iLE7QB!}?!?#vd@*A7Ldb!97J&a4|byR%i&zrYHkfAs?8L=aCJ zp&kDgX^vrQ1wz8HU}E)U6Ql%Yz3F|&F=9K3tpH4vfk?)a*($XEr?vlk`u_-I#7H&= zZpj&>5rmXxDhmpc1sCvDcOhFLQehOe7o3y^Di;Fe!o$pz|Aozhp7jWIdz8(pq$pHL z;Z;&JP)SjxGNK#_@izCpg{=xX@h(&(MGKV_U?Q+JP)X4SNl|CoAT2`EK;=anl^1PP zURdNsB{QwfL4;@(v|z<}A+n<@DZ2&TOuL{va-;=vq^BtldIh~eQRoe>O0mk6VwEYy zNRtzZ9RRLMQ zMRjRn7ooAKP;8eb_Bk{+6^if@AP1LHt7YhL(jbv7$G-wyP8v+ImH1bo(@BF)wi^E$ zbUSHq%AUvnS9CmSyVkCyo+2fSR7w`Bl&n-KS*cR8Ql(@iHcCML!b-^wsMsm-pqPC| zJM6N%xX0agH~u|#5B|^X=lH*{U*H$im}V+fo2gW7rczaq&RF%YQnisv)gqOuMJiQ` zRH_!KR4r1eTBK6-V5BO#FRUiD$98iE5JyVDM(b!AyH1X^b%Hp9zq4bFo#4*k?}8LA zLkf3Cvw5UD(zJIykjSMfky)#cL@pJHOzapp#Lu(fgMZJNFm&7gHqIZIt;KYr8sv z;070g;x?D-KH;7q%{(`c^b1^sO!y2}T<8{JdvFnm8I3@8dzLFc2eN#DTa5ND&D&xr zS6SwknFek-y1WXoa#nElm2M>n8>`UlrQK?`+O*(}@*?3cf#hU-=DcIpLpQpO+|ef1n_0Knv2FodFoO>h(82>XI98r zf2cnce-~EDS$~*64FBP*n6v%}e+2%ntemsh_UeYeJ1gj{KhhtGzlZOEzo+ksznAZY zzqjv=|0pp3Q~qdlsA=EF_rd=ybg5~7j6VkdvFKFOzOV0#|J(lC_>c3);XfW7YucaS zPr%>L_ru>G%>R@>5uIz=5AXxr;&3k^HA^3;-q41o5ZDPeLPK z=qH2xQ{t!aCrF{63Ib4xznVWn3S|tzTEuJoHKrZ*a;`Nk6hG7Z8~hDu8E^DA5)OhA zYYae8BG+_3-FSaDs|&54;Td=Pd;C4z*Y82u5By9&lRAK@WSTL;cmSKe5Bdk;fofk( zEci;$TJ)ISKja^xOi-4fpe&h6|CoP_@VS02^a=k2Dd+ilpcFmnpQHrPmnf652(?}0 z7jbvbf?w3iKgW84LcbUsqgI~3*YGd(OU=Q8&14$;6@CTl09N{yl(q`=ky07E$UiQi zkd(^U#k7{Oi)k-o7yRHf5e`lhX~1dXOCaD5Kh2rdP3Cu7@Lq25<*$@_LgdTDX2PRm z`)n#XmtD|AiWtEh#Au}?nZwX@e9ci0+2oiwKr9)zfVfH!a|4W*3Zc!QIdiE=@^9pe z>%FhcHd~{}r{AIbO8lkp0`6T}CvkEwau?!UbjZ-?{`1Q(CVz9{i!*5&>#;c_GI3Q( z_a;(mGj?gcgOy4Muxa4qNtigHkuT6R5|*??evKtgVm(hU#_NpGWRtdu(g=-8P0kx< z>LMZ0T_o3*{1PwWRn#uuHK%4tO_Oq+=7~5a&LM3iE~!zJI||(wZ3BmkMx!ZwE>w<@ zYmxQsEARhAxbXD;9QKg;QWvdJr5d3~X-Q1C*Hk$abJ^uFFiQk zbCWSzMmp#3(j()&h`~y#S)J-W;y5CnRddWdn!Tp$X&~mlC~9T zjh32L5SPNAFTbNF(~f7K>!PwOE!-Hs5Xl_9A@^l?(-da+NJh*1I4Pr$GElS*-Pt2 z?Gs&DgQ!n*f6-Y&q_;_ZWIR-;yF6j(L5aK+*_)i<&2u7`O_qC;sC;P~^|v&f zA4zm6Q|ee0Tw-rGejixwr$l!Kmb>WFq|Jpt)3jql>RpPgK>JH=W2AQGWBQ$UQkB_q z;)pY#14s133d+#oL#oW-+DQ%0NHM~@yq(M-Q|+!dFZnU`KQVZ3=@?Bt6_ zn>hqrY#vwol+xwnuDW`>d12gy(c{dkO4ljfMDS$u{sf8rc%sl9Q^t>;Xm(GT(*4N5 zKzjrQ(4IjFv{z6L?HyD?j|y5tj}AIaC0EczTM)vNjJQ_%jkKz~*&WiQLdTea_7i#1 zg}O_6@k>gP9|5vTsPrLPe4WJl_`P zDBr!o$LX2rm8Dme{16o ze=dKKoRS+1>0r0n9lU2J1%C@(2>u#8AFK&h2djdW!HQrx^KU)uk<7()4&Dvk3AO~A zgH6odZ3xx}Z!@E}E_f3J<+aT4y%xM0yb`<|yo42q_k$1Y6nnLuYQJYE+wa>E*o4^1 zJl{vb$H6C9p9bR=`w%;@4zV-%SMXV|E7%?E!AiszK^+Yo_&RH`6X7j(;;|Hw#!f_$ zEw;znhRh6>+A{wcHY1wYrnVVYBPy_D-5eXulu$;&K>VgaQ$3==9mV!liWaevOC3{>IS*NZU{3@!`yIpnmgT%a3h&{I@6uy z&UWXxbKQCFe0PDn(2a86aTmFZ-6d|c58b71j2r7NbC;BDM=YH(2cQ?2j-A~+4-AxQ{-YwJ+pqTLZ}waKJN{k&UJ&wC`T^ghTm3fw zk^k6#;{VRK>2|DF?BMHkC)VmchSUrEzx*!0+kb9b_%Hk(TVl&o#VL6Q|G&kEyZ;Yb zj7WO^IuOJseUm-x2*$NP2k`M(PXXL|l_;n%pw z2O%O=_*QVQ>+SiulA>NIP=N&a-Z$>|H-4R-|KINSU+(p<)4!#cN4-0e0Uv5_{)hBq zwL6W7oxqo$zVSn>qa-5UzlH+Tg~nK2Q%M1g}&-(^NbN(|7Xo}tf^kYde^P&p^3uL`o3J)5fi|U1iqCUYf zLEoT1`lf-wpkQclp1l^E>N67S?sg`+oO|tk_K)^{`+$AWR@)jhI%`xqLX zd1!AIps87eR%Qu$m=)+;)}UYcn|;Z?VqZhY@}_;uuD2Vp1plsm9}Do?u>SscG%7pL zr0ha#@`Vl2kvNxf4O}7i+#9-5ti3mJ&0K|R?hbaXTpQOGy+KNk+#qh6s3C(zoUNBH&pk;XlX;AC?n- zQ4(%9=7ftip-egM`JC|XIpHtlgjXiv{!|?$Rhv0fszY!Hqn)h0p^gRiV(d6wVn^GH z)T&Z}@TA>{)%X|4DPII?)u^Bc7Aq#3KbnWIGqlot9yCIeIS5-+*9Es?b808!gm$*C z9fm!i-y*>`*zM??y7IOk#9MY6TAvx%)Op@*@I}nR5A>HaPkkrzzOVc3sg|j(sXnRx zsUfMeQrD+urCv_e6?CRW!B1x-RfV4UaC<%XP$L=#Qwth^HPyZfIvX?qYi7yJHg^{= z3;h7TTJfP9BnLZqU(8`!Yz|awTz8idZSr82f?7 zY}FXiQP;9l1GhoKy=bb@c`*`==Fd9Ve@M9fr=~~WWorFzpw;#k=qyIKWsHhjGdIu$ zZTwNp(mO(H>}`?@od)Uct&$#{hSc>A=qxk?af{BwR|6juQO-f8rLB{57+G3$KK@$& z2Thx)y_2ul8ds$83nZRhY7h1(IiAswz3a3nQ>Xusyl6wDPSW12oq*0}9f{QGK4|@>U-=czYS1JQgBFh5d95To!YT}nT`RF@SL8`gg~A_NM_JcUgBC(+ozEx2 z2{q`tq-Haq)#$pUwd9%3@)8p9--Gxf{*!x;`ERz~yPWM^uG~`%Ix8vnUT8HsE2-77 z&{_Vs(5PH(8Ra`^PrZWn>nzv!KM`JI-`6WWtZ8C?`cPxGY0M)U6W#4b2}gq?<>m6z zc1^!S)6bPyKOI_)j$T?Szg^^c)SwfS`g51me<*ae74DwpXF{X;KZ!4wi$XbffnGy; zYAyOFNgF@Ql*Sip{L>Q8E?iWqAqzDmzb10eHGZ_jSm{sIehhTBYX+U={|JqFX9@mV zbWn0P@%8A}wrplQdTPay2?Rc}iD9W8Ue6zXsiv)D`*T4wG~B9g+1N zp7o*IBsRCzg>!1G$ggUDGIX|m6FSTPCv;A3N`DR^HU1)Ktv?T1ja6`|%_!)syp-n> zQp2C3lJb0LHGhsu%I`q)Q;sF1#$N@k^_N4d{gu$!ejGGEtzn*{c#xU?^3J>>Vp3yX?1=9b~2D1&yN!f+i67IT+a39Pk4O(Sk)Wk8GNhE340`FUZVY8bX9vu)6lRWpZ+<;HAC5g}b|H$p6}Iy@(azozX; ztnC?SPqC-k%k1U$3VWsft{rE`ld6OjU!8fTJ!$!9EIT+ea!tx+J%PQ-POwrp$5S3` zugodGE4FaXv!m=e_Dp-WJ`*(5_kK$|(n=3zodWZT#*VdP zS#>kfPDK0sD=&Y7C)cAyf2W;D_^k}?NTqRo_e zrX9swEw2>na$!!*&ZSo8!JWDG8^wBKQ*z1Jg!1AVa}DZCEpuuib)cqsSMay_+mkfZ z^A4S>s)Q#F)-qecP0h$992e>AS_*%0rQtlWk=#X^b{oYSsWIGDlWgg5Z*T)=SOp?y z#4|GPlu_t7Jr{ieGoQho!JR}e36@c68{3f}QvU_F*%T-M literal 0 HcmV?d00001 diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/config.yml b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/config.yml new file mode 100644 index 00000000..6d15162e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/resources/config.yml @@ -0,0 +1,70 @@ +# 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.22#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 + +# internet availability. +offline: false + +# http timeout in seconds. +timeout: 5 diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/stat.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/stat.go new file mode 100644 index 00000000..ba788b4c --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/text.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/text.go new file mode 100644 index 00000000..f31c515f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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, fillFont bool) string { + if font.IsUserFont(fontName) && !fillFont { + 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, false) + 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, true, 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, false) + 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, false) + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/version.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/version.go new file mode 100644 index 00000000..f70b94a9 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/version.go @@ -0,0 +1,109 @@ +/* +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/pkg/errors" +) + +// VersionStr is the current pdfcpu version. +var VersionStr = "v0.9.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) +} + +func identicalMajorAndMinorVersions(v1, v2 string) bool { + ss1 := strings.Split(v1, ".") + if len(ss1) < 2 { + return false + } + + ss2 := strings.Split(v2, ".") + if len(ss2) < 2 { + return false + } + + return ss1[0] == ss2[0] && ss1[1] == ss2[1] +} + +// CheckConfigVersion prints a warning if the configuration is outdated. +func CheckConfigVersion(v string) { + + if identicalMajorAndMinorVersions(v, VersionStr) { + return + } + + if log.CLIEnabled() { + log.CLI.Println(` +**************************** WARNING **************************** +* Your configuration is not based on the current major version. * +* Please backup and then reset your configuration: * +* $ pdfcpu config reset * +*****************************************************************`) + } +} diff --git a/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/watermark.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/watermark.go new file mode 100644 index 00000000..af21780a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/xreftable.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/xreftable.go new file mode 100644 index 00000000..180cb52a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/xreftable.go @@ -0,0 +1,2815 @@ +/* + 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 // from trailer dict. + MaxObjNr int // after reading in all objects from xRef table. + 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 ??? + AAPLExtensions bool // File is using Apple extensions for annotations and keywords. + + // 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.EscapedUTF16String(f) + if err != nil { + return nil, err + } + d.InsertString("F", *s) + + if s, err = types.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/zoom.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model/zoom.go new file mode 100644 index 00000000..d2b72b2c --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/nup.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/nup.go new file mode 100644 index 00000000..affe2c74 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/optimize.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/optimize.go new file mode 100644 index 00000000..a30953d1 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/optimize.go @@ -0,0 +1,1554 @@ +/* +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, pageNr 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 pageNr. + pageFonts := ctx.Optimize.PageFonts[pageNr] + + // 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 pageNr. + // 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, pageNr int) types.IntSet { + pageImages := ctx.Optimize.PageImages[pageNr] + if pageImages == nil { + pageImages = types.IntSet{} + ctx.Optimize.PageImages[pageNr] = pageImages + } + + return pageImages +} + +func pageFonts(ctx *model.Context, pageNr int) types.IntSet { + pageFonts := ctx.Optimize.PageFonts[pageNr] + if pageFonts == nil { + pageFonts = types.IntSet{} + ctx.Optimize.PageFonts[pageNr] = 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, pageNr, pageObjNumber int, rNamePrefix string) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict begin: page=%d pageObjNumber=%d %s\nPageFonts=%v\n", pageNr, pageObjNumber, rDict, ctx.Optimize.PageFonts) + } + + pageFonts := pageFonts(ctx, pageNr) + + // Iterate over font resource dict. + for rName, v := range rDict { + + indRef, ok := v.(types.IndirectRef) + if !ok { + continue + } + + objNr := int(indRef.ObjectNumber) + + qualifiedRName := rName + if rNamePrefix != "" { + qualifiedRName = rNamePrefix + "." + rName + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeFontResourcesDict: processing font: %s, obj#=%d\n", qualifiedRName, 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, qualifiedRName, objNr, pageNr) + 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{qualifiedRName}, + 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, pageNr int) (*int, error) { + // Get the set of image object numbers for pageNr. + pageImages := ctx.Optimize.PageImages[pageNr] + + // 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 pageNr. + // 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(pageNr, 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, rNamePrefix, rName string, rDict types.Dict, objNr, pageNr int, pageImages types.IntSet) error { + + qualifiedRName := rName + if rNamePrefix != "" { + qualifiedRName = rNamePrefix + "." + rName + } + + // Check if image is a duplicate and if so return the object number of the original. + originalObjNr, err := handleDuplicateImageObject(ctx, osd, qualifiedRName, objNr, pageNr) + if err != nil { + return 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) + rDict[rName] = *ir + return 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: map[int]string{pageNr: qualifiedRName}, + ImageDict: osd, + } + + pageImages[objNr] = true + return 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, pageNr, pageObjNumber int, rName string, 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, pageNr, pageObjNumber, rName, 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, rNamePrefix, rName string, rDict types.Dict, objNr, pageNr, 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) + } + + qualifiedRName := rName + if rNamePrefix != "" { + qualifiedRName = rNamePrefix + "." + rName + } + + return optimizeFormResources(ctx, o, pageNr, pageObjNumber, qualifiedRName, vis) +} + +func optimizeXObjectResourcesDict(ctx *model.Context, rDict types.Dict, pageNr, pageObjNumber int, rNamePrefix string, vis []types.Object) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeXObjectResourcesDict page#%dbegin: %s\n", pageObjNumber, rDict) + } + + pageImages := pageImages(ctx, pageNr) + + 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) + + qualifiedRName := rName + if rNamePrefix != "" { + qualifiedRName = rNamePrefix + "." + rName + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeXObjectResourcesDict: processing XObject: %s, obj#=%d\n", qualifiedRName, 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.Subtype() == "Image" { + if err := optimizeXObjectImage(ctx, sd, rNamePrefix, rName, rDict, objNr, pageNr, pageImages); err != nil { + return err + } + } + + if *sd.Subtype() == "Form" { + if err := optimizeForm(ctx, sd, rNamePrefix, rName, rDict, objNr, pageNr, 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, pageNr, pageObjNumber int, rNamePrefix string, visitedRes []types.Object) error { + if log.OptimizeEnabled() { + log.Optimize.Printf("optimizeResources begin: pageNr=%d pageObjNumber=%d\n", pageNr, 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", pageNr, pageObjNumber) + } + + if err = optimizeFontResourcesDict(ctx, d, pageNr, pageObjNumber, rNamePrefix); 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", pageNr, pageObjNumber) + } + + if err = optimizeXObjectResourcesDict(ctx, d, pageNr, pageObjNumber, rNamePrefix, 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, pageNr, pageObjNumber int) error { + if ctx.Optimize.Cache[pageObjNumber] { + return nil + } + ctx.Optimize.Cache[pageObjNumber] = true + + // The logical pageNr is pageNr+1. + if log.OptimizeEnabled() { + log.Optimize.Printf("parseResourcesDict begin page: %d, object:%d\n", pageNr+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, pageNr, pageObjNumber, "", []types.Object{}); err != nil { + return err + } + + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parseResourcesDict end page: %d, object:%d\n", pageNr+1, pageObjNumber) + } + + return nil +} + +// Iterate over all pages and optimize content & resources. +func parsePagesDict(ctx *model.Context, pagesDict types.Dict, pageNr int) (int, error) { + // TODO Integrate resource consolidation based on content stream requirements. + + count, found := pagesDict.Find("Count") + if !found { + return pageNr, errors.New("pdfcpu: parsePagesDict: missing Count") + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parsePagesDict begin (next page=%d has %s pages): %s\n", pageNr+1, count.(types.Integer), pagesDict) + } + + ctx.Optimize.Cache = map[int]bool{} + + // Iterate over page tree. + o, found := pagesDict.Find("Kids") + if !found { + return pageNr, errors.Errorf("pdfcpu: corrupt \"Kids\" entry %s", pagesDict) + } + + kids, err := ctx.DereferenceArray(o) + if err != nil || kids == nil { + return pageNr, errors.Errorf("pdfcpu: corrupt \"Kids\" entry: %s", pagesDict) + } + + 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. + pageNr, err = parsePagesDict(ctx, d, pageNr) + 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, pageNr, int(ir.ObjectNumber)); err != nil { + return 0, err + } + + pageNr++ + } + + if log.OptimizeEnabled() { + log.Optimize.Printf("parsePagesDict end: %s\n", pagesDict) + } + + return pageNr, 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/page.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/page.go new file mode 100644 index 00000000..09c33810 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/band.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/band.go new file mode 100644 index 00000000..5f6ea736 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/bar.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/bar.go new file mode 100644 index 00000000..7acb6a53 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/border.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/border.go new file mode 100644 index 00000000..1b05878b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/buttons.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/buttons.go new file mode 100644 index 00000000..606b5566 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/checkBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/checkBox.go new file mode 100644 index 00000000..d7d4949a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/comboBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/comboBox.go new file mode 100644 index 00000000..acfb1a03 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/comboBox.go @@ -0,0 +1,814 @@ +/* + 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") + } + + fillFont := formFontIndRef(ctx.XRefTable, fontID) != nil + + cb.fontID = id + cb.Font.Name = name + cb.Font.Lang = lang + cb.Font.FillFont = fillFont + 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, f.FillFont) + 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.EscapedUTF16String(cb.ID) + if err != nil { + return nil, err + } + + opt := types.Array{} + for _, s := range cb.Options { + s, err := types.EscapedUTF16String(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.EscapedUTF16String(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.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/content.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/content.go new file mode 100644 index 00000000..4765103f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/date.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/date.go new file mode 100644 index 00000000..4080e98f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/dateField.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/dateField.go new file mode 100644 index 00000000..2b383330 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/dateField.go @@ -0,0 +1,950 @@ +/* + 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") + } + + fillFont := formFontIndRef(ctx.XRefTable, fontID) != nil + + df.fontID = id + df.Font.Name = name + df.Font.Lang = lang + df.Font.FillFont = fillFont + + 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, f.FillFont) + 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.EscapedUTF16String(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.EscapedUTF16String(df.Value) + if err != nil { + return nil, err + } + d["V"] = types.StringLiteral(*s) + } + + if df.Default != "" { + s, err := types.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/divider.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/divider.go new file mode 100644 index 00000000..abcc6288 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/fieldGroup.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/fieldGroup.go new file mode 100644 index 00000000..b10ebb69 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/font.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/font.go new file mode 100644 index 00000000..01bb9a6f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/font.go @@ -0,0 +1,340 @@ +/* + 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 + FillFont bool +} + +// 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/guide.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/guide.go new file mode 100644 index 00000000..9bb7d775 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/imageBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/imageBox.go new file mode 100644 index 00000000..b61cf408 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/imageBox.go @@ -0,0 +1,692 @@ +/* + 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" + "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") { + if pdf.Offline { + if log.CLIEnabled() { + log.CLI.Printf("pdfcpu is offline, can't get %s\n", ib.Src) + } + return nil, nil + } + client := pdf.httpClient + if client == nil { + pdf.httpClient = &http.Client{ + Timeout: time.Duration(pdf.Timeout) * time.Second, + } + client = pdf.httpClient + } + resp, err := client.Get(ib.Src) + if err != nil { + if e, ok := err.(net.Error); ok && e.Timeout() { + if log.CLIEnabled() { + log.CLI.Printf("timeout: %s\n", ib.Src) + } + } else { + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/listBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/listBox.go new file mode 100644 index 00000000..b7671a29 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/listBox.go @@ -0,0 +1,1018 @@ +/* + 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") + } + + fillFont := formFontIndRef(ctx.XRefTable, fontID) != nil + + lb.fontID = id + lb.Font.Name = name + lb.Font.Lang = lang + lb.Font.FillFont = fillFont + 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, f.FillFont) + 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.EscapedUTF16String(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.EscapedUTF16String(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.EscapedUTF16String(lb.ID) + if err != nil { + return nil, err + } + + opt := types.Array{} + for _, s := range lb.Options { + s1, err := types.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/margin.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/margin.go new file mode 100644 index 00000000..c40e41ab --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/padding.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/padding.go new file mode 100644 index 00000000..7fbd0f05 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/page.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/page.go new file mode 100644 index 00000000..a25d6217 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/pdf.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/pdf.go new file mode 100644 index 00000000..0bbf3932 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/pdf.go @@ -0,0 +1,1135 @@ +/* + 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:"-"` + Offline bool `json:"-"` + Timeout int `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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/radioButtonGroup.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/radioButtonGroup.go new file mode 100644 index 00000000..44c56326 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/regions.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/regions.go new file mode 100644 index 00000000..82b2023a --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/simpleBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/simpleBox.go new file mode 100644 index 00000000..cc50d88b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/table.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/table.go new file mode 100644 index 00000000..a277cbd7 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textBox.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textBox.go new file mode 100644 index 00000000..61fdd0fe --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textField.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textField.go new file mode 100644 index 00000000..395f1e31 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textField.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 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") + } + + fillFont := formFontIndRef(ctx.XRefTable, fontID) != nil + + 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.Font.FillFont = fillFont + 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(), f.FillFont) + 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.EscapedUTF16String(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.EscapedUTF16String(tf.Tip) + if err != nil { + return nil, err + } + d["TU"] = types.StringLiteral(*tu) + } + + tf.handleBorderAndMK(d) + + if tf.Value != "" { + s, err := types.EscapedUTF16String(tf.Value) + if err != nil { + return nil, err + } + d["V"] = types.StringLiteral(*s) + } + + if tf.Default != "" { + s, err := types.EscapedUTF16String(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 + } + + obj, ok := sd.Find("Resources") + if !ok { + return renderTextFieldAP(ctx, d, text, multiLine, fonts) + } + + d1, err := ctx.DereferenceDict(obj) + if err != nil { + return err + } + 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 + } + + fillFont := formFontIndRef(ctx.XRefTable, fontID) != nil + + 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.Font.FillFont = fillFont + 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textFieldLabel.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/primitives/textFieldLabel.go new file mode 100644 index 00000000..3098b53f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/property.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/property.go new file mode 100644 index 00000000..d2a96930 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/read.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/read.go new file mode 100644 index 00000000..6ba56234 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/read.go @@ -0,0 +1,3031 @@ +/* +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 ctx.Read.FileSize == 0 { + return nil, errors.New("The file could not be opened because it is empty.") + } + + 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 != ctx.MaxObjNr+1 { + *ctx.XRefTable.Size = ctx.MaxObjNr + 1 + model.ShowRepaired("trailer size") + } + + 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("the file may be damaged.") + } + + 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 objNr == 0 { + entry.Free = true + model.ShowRepaired("obj#0") + return entry, true + } + 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) { + // Fix for #250 + if *ctx.Size == len(ctx.Table)+1 { + // 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 { + // 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) + } + model.ShowRepaired("obj#0") + } +} + +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 +} + +func filterArraySupportsDecodeParms(filters types.Array) bool { + for _, obj := range filters { + if name, ok := obj.(types.Name); ok { + if filter.SupportsDecodeParms(name.String()) { + return true + } + } + } + return false +} + +// 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 { + if filterArraySupportsDecodeParms(filterArray) { + decodeParmsArr, ok = decodeParms.(types.Array) + if ok { + if 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 !fixLength && 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 { + if log.ReadEnabled() { + log.Read.Printf("dereferenceObject: begin, dereferencing object %d\n", objNr) + } + + if objNr > ctx.MaxObjNr { + ctx.MaxObjNr = objNr + } + + entry := ctx.Table[objNr] + + if entry.Free { + if log.ReadEnabled() { + log.Read.Printf("free object %d\n", objNr) + } + return nil + } + + if entry.Compressed { + if err := decompressXRefTableEntry(ctx.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, ctx.MaxObjNr+1, 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 +} + +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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/resize.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/resize.go new file mode 100644 index 00000000..b4def1fc --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/rotate.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/rotate.go new file mode 100644 index 00000000..76ea8100 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan/scan.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan/scan.go new file mode 100644 index 00000000..f7d9d2b7 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/stamp.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/stamp.go new file mode 100644 index 00000000..3ac18051 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.XRefTable.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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/anchor.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/anchor.go new file mode 100644 index 00000000..24325b59 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/array.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/array.go new file mode 100644 index 00000000..c5a265aa --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/date.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/date.go new file mode 100644 index 00000000..a2f7a214 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/dict.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/dict.go new file mode 100644 index 00000000..653d9927 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/layout.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/layout.go new file mode 100644 index 00000000..174a1ed9 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/paperSize.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/paperSize.go new file mode 100644 index 00000000..93ee9f3b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/slice.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/slice.go new file mode 100644 index 00000000..60d3ae5b --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/streamdict.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/streamdict.go new file mode 100644 index 00000000..7f4b890f --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/string.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/string.go new file mode 100644 index 00000000..9150346e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/types.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/types.go new file mode 100644 index 00000000..27961105 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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 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 nameObject.String() +} + +/////////////////////////////////////////////////////////////////////////////////// + +// 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/utf16.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types/utf16.go new file mode 100644 index 00000000..7af732df --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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 EscapedUTF16String(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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/write.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/write.go new file mode 100644 index 00000000..4d73d706 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.XRefTable.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.XRefTable.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.XRefTable.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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeImage.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeImage.go new file mode 100644 index 00000000..38425b18 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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.LZW, 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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeObjects.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeObjects.go new file mode 100644 index 00000000..d65efc58 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writePages.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writePages.go new file mode 100644 index 00000000..3f74781e --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeStats.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/writeStats.go new file mode 100644 index 00000000..89f93cc4 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/zoom.go b/vendor/github.com/pdfcpu/pdfcpu/pkg/pdfcpu/zoom.go new file mode 100644 index 00000000..379d2d75 --- /dev/null +++ b/vendor/github.com/pdfcpu/pdfcpu/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/vendor/golang.org/x/image/LICENSE b/vendor/golang.org/x/image/LICENSE new file mode 100644 index 00000000..2a7cf70d --- /dev/null +++ b/vendor/golang.org/x/image/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/golang.org/x/image/PATENTS b/vendor/golang.org/x/image/PATENTS new file mode 100644 index 00000000..73309904 --- /dev/null +++ b/vendor/golang.org/x/image/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google 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, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/image/ccitt/reader.go b/vendor/golang.org/x/image/ccitt/reader.go new file mode 100644 index 00000000..340de053 --- /dev/null +++ b/vendor/golang.org/x/image/ccitt/reader.go @@ -0,0 +1,795 @@ +// Copyright 2019 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. + +//go:generate go run gen.go + +// Package ccitt implements a CCITT (fax) image decoder. +package ccitt + +import ( + "encoding/binary" + "errors" + "image" + "io" + "math/bits" +) + +var ( + errIncompleteCode = errors.New("ccitt: incomplete code") + errInvalidBounds = errors.New("ccitt: invalid bounds") + errInvalidCode = errors.New("ccitt: invalid code") + errInvalidMode = errors.New("ccitt: invalid mode") + errInvalidOffset = errors.New("ccitt: invalid offset") + errMissingEOL = errors.New("ccitt: missing End-of-Line") + errRunLengthOverflowsWidth = errors.New("ccitt: run length overflows width") + errRunLengthTooLong = errors.New("ccitt: run length too long") + errUnsupportedMode = errors.New("ccitt: unsupported mode") + errUnsupportedSubFormat = errors.New("ccitt: unsupported sub-format") + errUnsupportedWidth = errors.New("ccitt: unsupported width") +) + +// Order specifies the bit ordering in a CCITT data stream. +type Order uint32 + +const ( + // LSB means Least Significant Bits first. + LSB Order = iota + // MSB means Most Significant Bits first. + MSB +) + +// SubFormat represents that the CCITT format consists of a number of +// sub-formats. Decoding or encoding a CCITT data stream requires knowing the +// sub-format context. It is not represented in the data stream per se. +type SubFormat uint32 + +const ( + Group3 SubFormat = iota + Group4 +) + +// AutoDetectHeight is passed as the height argument to NewReader to indicate +// that the image height (the number of rows) is not known in advance. +const AutoDetectHeight = -1 + +// Options are optional parameters. +type Options struct { + // Align means that some variable-bit-width codes are byte-aligned. + Align bool + // Invert means that black is the 1 bit or 0xFF byte, and white is 0. + Invert bool +} + +// maxWidth is the maximum (inclusive) supported width. This is a limitation of +// this implementation, to guard against integer overflow, and not anything +// inherent to the CCITT format. +const maxWidth = 1 << 20 + +func invertBytes(b []byte) { + for i, c := range b { + b[i] = ^c + } +} + +func reverseBitsWithinBytes(b []byte) { + for i, c := range b { + b[i] = bits.Reverse8(c) + } +} + +// highBits writes to dst (1 bit per pixel, most significant bit first) the +// high (0x80) bits from src (1 byte per pixel). It returns the number of bytes +// written and read such that dst[:d] is the packed form of src[:s]. +// +// For example, if src starts with the 8 bytes [0x7D, 0x7E, 0x7F, 0x80, 0x81, +// 0x82, 0x00, 0xFF] then 0x1D will be written to dst[0]. +// +// If src has (8 * len(dst)) or more bytes then only len(dst) bytes are +// written, (8 * len(dst)) bytes are read, and invert is ignored. +// +// Otherwise, if len(src) is not a multiple of 8 then the final byte written to +// dst is padded with 1 bits (if invert is true) or 0 bits. If inverted, the 1s +// are typically temporary, e.g. they will be flipped back to 0s by an +// invertBytes call in the highBits caller, reader.Read. +func highBits(dst []byte, src []byte, invert bool) (d int, s int) { + // Pack as many complete groups of 8 src bytes as we can. + n := len(src) / 8 + if n > len(dst) { + n = len(dst) + } + dstN := dst[:n] + for i := range dstN { + src8 := src[i*8 : i*8+8] + dstN[i] = ((src8[0] & 0x80) >> 0) | + ((src8[1] & 0x80) >> 1) | + ((src8[2] & 0x80) >> 2) | + ((src8[3] & 0x80) >> 3) | + ((src8[4] & 0x80) >> 4) | + ((src8[5] & 0x80) >> 5) | + ((src8[6] & 0x80) >> 6) | + ((src8[7] & 0x80) >> 7) + } + d, s = n, 8*n + dst, src = dst[d:], src[s:] + + // Pack up to 7 remaining src bytes, if there's room in dst. + if (len(dst) > 0) && (len(src) > 0) { + dstByte := byte(0) + if invert { + dstByte = 0xFF >> uint(len(src)) + } + for n, srcByte := range src { + dstByte |= (srcByte & 0x80) >> uint(n) + } + dst[0] = dstByte + d, s = d+1, s+len(src) + } + return d, s +} + +type bitReader struct { + r io.Reader + + // readErr is the error returned from the most recent r.Read call. As the + // io.Reader documentation says, when r.Read returns (n, err), "always + // process the n > 0 bytes returned before considering the error err". + readErr error + + // order is whether to process r's bytes LSB first or MSB first. + order Order + + // The high nBits bits of the bits field hold upcoming bits in MSB order. + bits uint64 + nBits uint32 + + // bytes[br:bw] holds bytes read from r but not yet loaded into bits. + br uint32 + bw uint32 + bytes [1024]uint8 +} + +func (b *bitReader) alignToByteBoundary() { + n := b.nBits & 7 + b.bits <<= n + b.nBits -= n +} + +// nextBitMaxNBits is the maximum possible value of bitReader.nBits after a +// bitReader.nextBit call, provided that bitReader.nBits was not more than this +// value before that call. +// +// Note that the decode function can unread bits, which can temporarily set the +// bitReader.nBits value above nextBitMaxNBits. +const nextBitMaxNBits = 31 + +func (b *bitReader) nextBit() (uint64, error) { + for { + if b.nBits > 0 { + bit := b.bits >> 63 + b.bits <<= 1 + b.nBits-- + return bit, nil + } + + if available := b.bw - b.br; available >= 4 { + // Read 32 bits, even though b.bits is a uint64, since the decode + // function may need to unread up to maxCodeLength bits, putting + // them back in the remaining (64 - 32) bits. TestMaxCodeLength + // checks that the generated maxCodeLength constant fits. + // + // If changing the Uint32 call, also change nextBitMaxNBits. + b.bits = uint64(binary.BigEndian.Uint32(b.bytes[b.br:])) << 32 + b.br += 4 + b.nBits = 32 + continue + } else if available > 0 { + b.bits = uint64(b.bytes[b.br]) << (7 * 8) + b.br++ + b.nBits = 8 + continue + } + + if b.readErr != nil { + return 0, b.readErr + } + + n, err := b.r.Read(b.bytes[:]) + b.br = 0 + b.bw = uint32(n) + b.readErr = err + + if b.order != MSB { + reverseBitsWithinBytes(b.bytes[:b.bw]) + } + } +} + +func decode(b *bitReader, decodeTable [][2]int16) (uint32, error) { + nBitsRead, bitsRead, state := uint32(0), uint64(0), int32(1) + for { + bit, err := b.nextBit() + if err != nil { + if err == io.EOF { + err = errIncompleteCode + } + return 0, err + } + bitsRead |= bit << (63 - nBitsRead) + nBitsRead++ + + // The "&1" is redundant, but can eliminate a bounds check. + state = int32(decodeTable[state][bit&1]) + if state < 0 { + return uint32(^state), nil + } else if state == 0 { + // Unread the bits we've read, then return errInvalidCode. + b.bits = (b.bits >> nBitsRead) | bitsRead + b.nBits += nBitsRead + return 0, errInvalidCode + } + } +} + +// decodeEOL decodes the 12-bit EOL code 0000_0000_0001. +func decodeEOL(b *bitReader) error { + nBitsRead, bitsRead := uint32(0), uint64(0) + for { + bit, err := b.nextBit() + if err != nil { + if err == io.EOF { + err = errMissingEOL + } + return err + } + bitsRead |= bit << (63 - nBitsRead) + nBitsRead++ + + if nBitsRead < 12 { + if bit&1 == 0 { + continue + } + } else if bit&1 != 0 { + return nil + } + + // Unread the bits we've read, then return errMissingEOL. + b.bits = (b.bits >> nBitsRead) | bitsRead + b.nBits += nBitsRead + return errMissingEOL + } +} + +type reader struct { + br bitReader + subFormat SubFormat + + // width is the image width in pixels. + width int + + // rowsRemaining starts at the image height in pixels, when the reader is + // driven through the io.Reader interface, and decrements to zero as rows + // are decoded. Alternatively, it may be negative if the image height is + // not known in advance at the time of the NewReader call. + // + // When driven through DecodeIntoGray, this field is unused. + rowsRemaining int + + // curr and prev hold the current and previous rows. Each element is either + // 0x00 (black) or 0xFF (white). + // + // prev may be nil, when processing the first row. + curr []byte + prev []byte + + // ri is the read index. curr[:ri] are those bytes of curr that have been + // passed along via the Read method. + // + // When the reader is driven through DecodeIntoGray, instead of through the + // io.Reader interface, this field is unused. + ri int + + // wi is the write index. curr[:wi] are those bytes of curr that have + // already been decoded via the decodeRow method. + // + // What this implementation calls wi is roughly equivalent to what the spec + // calls the a0 index. + wi int + + // These fields are copied from the *Options (which may be nil). + align bool + invert bool + + // atStartOfRow is whether we have just started the row. Some parts of the + // spec say to treat this situation as if "wi = -1". + atStartOfRow bool + + // penColorIsWhite is whether the next run is black or white. + penColorIsWhite bool + + // seenStartOfImage is whether we've called the startDecode method. + seenStartOfImage bool + + // truncated is whether the input is missing the final 6 consecutive EOL's + // (for Group3) or 2 consecutive EOL's (for Group4). Omitting that trailer + // (but otherwise padding to a byte boundary, with either all 0 bits or all + // 1 bits) is invalid according to the spec, but happens in practice when + // exporting from Adobe Acrobat to TIFF + CCITT. This package silently + // ignores the format error for CCITT input that has been truncated in that + // fashion, returning the full decoded image. + // + // Detecting trailer truncation (just after the final row of pixels) + // requires knowing which row is the final row, and therefore does not + // trigger if the image height is not known in advance. + truncated bool + + // readErr is a sticky error for the Read method. + readErr error +} + +func (z *reader) Read(p []byte) (int, error) { + if z.readErr != nil { + return 0, z.readErr + } + originalP := p + + for len(p) > 0 { + // Allocate buffers (and decode any start-of-image codes), if + // processing the first or second row. + if z.curr == nil { + if !z.seenStartOfImage { + if z.readErr = z.startDecode(); z.readErr != nil { + break + } + z.atStartOfRow = true + } + z.curr = make([]byte, z.width) + } + + // Decode the next row, if necessary. + if z.atStartOfRow { + if z.rowsRemaining < 0 { + // We do not know the image height in advance. See if the next + // code is an EOL. If it is, it is consumed. If it isn't, the + // bitReader shouldn't advance along the bit stream, and we + // simply decode another row of pixel data. + // + // For the Group4 subFormat, we may need to align to a byte + // boundary. For the Group3 subFormat, the previous z.decodeRow + // call (or z.startDecode call) has already consumed one of the + // 6 consecutive EOL's. The next EOL is actually the second of + // 6, in the middle, and we shouldn't align at that point. + if z.align && (z.subFormat == Group4) { + z.br.alignToByteBoundary() + } + + if err := z.decodeEOL(); err == errMissingEOL { + // No-op. It's another row of pixel data. + } else if err != nil { + z.readErr = err + break + } else { + if z.readErr = z.finishDecode(true); z.readErr != nil { + break + } + z.readErr = io.EOF + break + } + + } else if z.rowsRemaining == 0 { + // We do know the image height in advance, and we have already + // decoded exactly that many rows. + if z.readErr = z.finishDecode(false); z.readErr != nil { + break + } + z.readErr = io.EOF + break + + } else { + z.rowsRemaining-- + } + + if z.readErr = z.decodeRow(z.rowsRemaining == 0); z.readErr != nil { + break + } + } + + // Pack from z.curr (1 byte per pixel) to p (1 bit per pixel). + packD, packS := highBits(p, z.curr[z.ri:], z.invert) + p = p[packD:] + z.ri += packS + + // Prepare to decode the next row, if necessary. + if z.ri == len(z.curr) { + z.ri, z.curr, z.prev = 0, z.prev, z.curr + z.atStartOfRow = true + } + } + + n := len(originalP) - len(p) + if z.invert { + invertBytes(originalP[:n]) + } + return n, z.readErr +} + +func (z *reader) penColor() byte { + if z.penColorIsWhite { + return 0xFF + } + return 0x00 +} + +func (z *reader) startDecode() error { + switch z.subFormat { + case Group3: + if err := z.decodeEOL(); err != nil { + return err + } + + case Group4: + // No-op. + + default: + return errUnsupportedSubFormat + } + + z.seenStartOfImage = true + return nil +} + +func (z *reader) finishDecode(alreadySeenEOL bool) error { + numberOfEOLs := 0 + switch z.subFormat { + case Group3: + if z.truncated { + return nil + } + // The stream ends with a RTC (Return To Control) of 6 consecutive + // EOL's, but we should have already just seen an EOL, either in + // z.startDecode (for a zero-height image) or in z.decodeRow. + numberOfEOLs = 5 + + case Group4: + autoDetectHeight := z.rowsRemaining < 0 + if autoDetectHeight { + // Aligning to a byte boundary was already handled by reader.Read. + } else if z.align { + z.br.alignToByteBoundary() + } + // The stream ends with two EOL's. If the first one is missing, and we + // had an explicit image height, we just assume that the trailing two + // EOL's were truncated and return a nil error. + if err := z.decodeEOL(); err != nil { + if (err == errMissingEOL) && !autoDetectHeight { + z.truncated = true + return nil + } + return err + } + numberOfEOLs = 1 + + default: + return errUnsupportedSubFormat + } + + if alreadySeenEOL { + numberOfEOLs-- + } + for ; numberOfEOLs > 0; numberOfEOLs-- { + if err := z.decodeEOL(); err != nil { + return err + } + } + return nil +} + +func (z *reader) decodeEOL() error { + return decodeEOL(&z.br) +} + +func (z *reader) decodeRow(finalRow bool) error { + z.wi = 0 + z.atStartOfRow = true + z.penColorIsWhite = true + + if z.align { + z.br.alignToByteBoundary() + } + + switch z.subFormat { + case Group3: + for ; z.wi < len(z.curr); z.atStartOfRow = false { + if err := z.decodeRun(); err != nil { + return err + } + } + err := z.decodeEOL() + if finalRow && (err == errMissingEOL) { + z.truncated = true + return nil + } + return err + + case Group4: + for ; z.wi < len(z.curr); z.atStartOfRow = false { + mode, err := decode(&z.br, modeDecodeTable[:]) + if err != nil { + return err + } + rm := readerMode{} + if mode < uint32(len(readerModes)) { + rm = readerModes[mode] + } + if rm.function == nil { + return errInvalidMode + } + if err := rm.function(z, rm.arg); err != nil { + return err + } + } + return nil + } + + return errUnsupportedSubFormat +} + +func (z *reader) decodeRun() error { + table := blackDecodeTable[:] + if z.penColorIsWhite { + table = whiteDecodeTable[:] + } + + total := 0 + for { + n, err := decode(&z.br, table) + if err != nil { + return err + } + if n > maxWidth { + panic("unreachable") + } + total += int(n) + if total > maxWidth { + return errRunLengthTooLong + } + // Anything 0x3F or below is a terminal code. + if n <= 0x3F { + break + } + } + + if total > (len(z.curr) - z.wi) { + return errRunLengthOverflowsWidth + } + dst := z.curr[z.wi : z.wi+total] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi += total + z.penColorIsWhite = !z.penColorIsWhite + + return nil +} + +// The various modes' semantics are based on determining a row of pixels' +// "changing elements": those pixels whose color differs from the one on its +// immediate left. +// +// The row above the first row is implicitly all white. Similarly, the column +// to the left of the first column is implicitly all white. +// +// For example, here's Figure 1 in "ITU-T Recommendation T.6", where the +// current and previous rows contain black (B) and white (w) pixels. The a? +// indexes point into curr, the b? indexes point into prev. +// +// b1 b2 +// v v +// prev: BBBBBwwwwwBBBwwwww +// curr: BBBwwwwwBBBBBBwwww +// ^ ^ ^ +// a0 a1 a2 +// +// a0 is the "reference element" or current decoder position, roughly +// equivalent to what this implementation calls reader.wi. +// +// a1 is the next changing element to the right of a0, on the "coding line" +// (the current row). +// +// a2 is the next changing element to the right of a1, again on curr. +// +// b1 is the first changing element on the "reference line" (the previous row) +// to the right of a0 and of opposite color to a0. +// +// b2 is the next changing element to the right of b1, again on prev. +// +// The various modes calculate a1 (and a2, for modeH): +// - modePass calculates that a1 is at or to the right of b2. +// - modeH calculates a1 and a2 without considering b1 or b2. +// - modeV* calculates a1 to be b1 plus an adjustment (between -3 and +3). + +const ( + findB1 = false + findB2 = true +) + +// findB finds either the b1 or b2 value. +func (z *reader) findB(whichB bool) int { + // The initial row is a special case. The previous row is implicitly all + // white, so that there are no changing pixel elements. We return b1 or b2 + // to be at the end of the row. + if len(z.prev) != len(z.curr) { + return len(z.curr) + } + + i := z.wi + + if z.atStartOfRow { + // a0 is implicitly at -1, on a white pixel. b1 is the first black + // pixel in the previous row. b2 is the first white pixel after that. + for ; (i < len(z.prev)) && (z.prev[i] == 0xFF); i++ { + } + if whichB == findB2 { + for ; (i < len(z.prev)) && (z.prev[i] == 0x00); i++ { + } + } + return i + } + + // As per figure 1 above, assume that the current pen color is white. + // First, walk past every contiguous black pixel in prev, starting at a0. + oppositeColor := ^z.penColor() + for ; (i < len(z.prev)) && (z.prev[i] == oppositeColor); i++ { + } + + // Then walk past every contiguous white pixel. + penColor := ^oppositeColor + for ; (i < len(z.prev)) && (z.prev[i] == penColor); i++ { + } + + // We're now at a black pixel (or at the end of the row). That's b1. + if whichB == findB2 { + // If we're looking for b2, walk past every contiguous black pixel + // again. + oppositeColor := ^penColor + for ; (i < len(z.prev)) && (z.prev[i] == oppositeColor); i++ { + } + } + + return i +} + +type readerMode struct { + function func(z *reader, arg int) error + arg int +} + +var readerModes = [...]readerMode{ + modePass: {function: readerModePass}, + modeH: {function: readerModeH}, + modeV0: {function: readerModeV, arg: +0}, + modeVR1: {function: readerModeV, arg: +1}, + modeVR2: {function: readerModeV, arg: +2}, + modeVR3: {function: readerModeV, arg: +3}, + modeVL1: {function: readerModeV, arg: -1}, + modeVL2: {function: readerModeV, arg: -2}, + modeVL3: {function: readerModeV, arg: -3}, + modeExt: {function: readerModeExt}, +} + +func readerModePass(z *reader, arg int) error { + b2 := z.findB(findB2) + if (b2 < z.wi) || (len(z.curr) < b2) { + return errInvalidOffset + } + dst := z.curr[z.wi:b2] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi = b2 + return nil +} + +func readerModeH(z *reader, arg int) error { + // The first iteration finds a1. The second finds a2. + for i := 0; i < 2; i++ { + if err := z.decodeRun(); err != nil { + return err + } + } + return nil +} + +func readerModeV(z *reader, arg int) error { + a1 := z.findB(findB1) + arg + if (a1 < z.wi) || (len(z.curr) < a1) { + return errInvalidOffset + } + dst := z.curr[z.wi:a1] + penColor := z.penColor() + for i := range dst { + dst[i] = penColor + } + z.wi = a1 + z.penColorIsWhite = !z.penColorIsWhite + return nil +} + +func readerModeExt(z *reader, arg int) error { + return errUnsupportedMode +} + +// DecodeIntoGray decodes the CCITT-formatted data in r into dst. +// +// It returns an error if dst's width and height don't match the implied width +// and height of CCITT-formatted data. +func DecodeIntoGray(dst *image.Gray, r io.Reader, order Order, sf SubFormat, opts *Options) error { + bounds := dst.Bounds() + if (bounds.Dx() < 0) || (bounds.Dy() < 0) { + return errInvalidBounds + } + if bounds.Dx() > maxWidth { + return errUnsupportedWidth + } + + z := reader{ + br: bitReader{r: r, order: order}, + subFormat: sf, + align: (opts != nil) && opts.Align, + invert: (opts != nil) && opts.Invert, + width: bounds.Dx(), + } + if err := z.startDecode(); err != nil { + return err + } + + width := bounds.Dx() + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + p := (y - bounds.Min.Y) * dst.Stride + z.curr = dst.Pix[p : p+width] + if err := z.decodeRow(y+1 == bounds.Max.Y); err != nil { + return err + } + z.curr, z.prev = nil, z.curr + } + + if err := z.finishDecode(false); err != nil { + return err + } + + if z.invert { + for y := bounds.Min.Y; y < bounds.Max.Y; y++ { + p := (y - bounds.Min.Y) * dst.Stride + invertBytes(dst.Pix[p : p+width]) + } + } + + return nil +} + +// NewReader returns an io.Reader that decodes the CCITT-formatted data in r. +// The resultant byte stream is one bit per pixel (MSB first), with 1 meaning +// white and 0 meaning black. Each row in the result is byte-aligned. +// +// A negative height, such as passing AutoDetectHeight, means that the image +// height is not known in advance. A negative width is invalid. +func NewReader(r io.Reader, order Order, sf SubFormat, width int, height int, opts *Options) io.Reader { + readErr := error(nil) + if width < 0 { + readErr = errInvalidBounds + } else if width > maxWidth { + readErr = errUnsupportedWidth + } + + return &reader{ + br: bitReader{r: r, order: order}, + subFormat: sf, + align: (opts != nil) && opts.Align, + invert: (opts != nil) && opts.Invert, + width: width, + rowsRemaining: height, + readErr: readErr, + } +} diff --git a/vendor/golang.org/x/image/ccitt/table.go b/vendor/golang.org/x/image/ccitt/table.go new file mode 100644 index 00000000..8b3794bc --- /dev/null +++ b/vendor/golang.org/x/image/ccitt/table.go @@ -0,0 +1,972 @@ +// generated by "go run gen.go". DO NOT EDIT. + +package ccitt + +// Each decodeTable is represented by an array of [2]int16's: a binary tree. +// Each array element (other than element 0, which means invalid) is a branch +// node in that tree. The root node is always element 1 (the second element). +// +// To walk the tree, look at the next bit in the bit stream, using it to select +// the first or second element of the [2]int16. If that int16 is 0, we have an +// invalid code. If it is positive, go to that branch node. If it is negative, +// then we have a leaf node, whose value is the bitwise complement (the ^ +// operator) of that int16. +// +// Comments above each decodeTable also show the same structure visually. The +// "b123" lines show the 123'rd branch node. The "=XXXXX" lines show an invalid +// code. The "=v1234" lines show a leaf node with value 1234. When reading the +// bit stream, a 0 or 1 bit means to go up or down, as you move left to right. +// +// For example, in modeDecodeTable, branch node b005 is three steps up from the +// root node, meaning that we have already seen "000". If the next bit is "0" +// then we move to branch node b006. Otherwise, the next bit is "1", and we +// move to the leaf node v0000 (also known as the modePass constant). Indeed, +// the bits that encode modePass are "0001". +// +// Tables 1, 2 and 3 come from the "ITU-T Recommendation T.6: FACSIMILE CODING +// SCHEMES AND CODING CONTROL FUNCTIONS FOR GROUP 4 FACSIMILE APPARATUS" +// specification: +// +// https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-T.6-198811-I!!PDF-E&type=items + +// modeDecodeTable represents Table 1 and the End-of-Line code. +// +// +=XXXXX +// b009 +-+ +// | +=v0009 +// b007 +-+ +// | | +=v0008 +// b010 | +-+ +// | +=v0005 +// b006 +-+ +// | | +=v0007 +// b008 | +-+ +// | +=v0004 +// b005 +-+ +// | +=v0000 +// b003 +-+ +// | +=v0001 +// b002 +-+ +// | | +=v0006 +// b004 | +-+ +// | +=v0003 +// b001 +-+ +// +=v0002 +var modeDecodeTable = [...][2]int16{ + 0: {0, 0}, + 1: {2, ^2}, + 2: {3, 4}, + 3: {5, ^1}, + 4: {^6, ^3}, + 5: {6, ^0}, + 6: {7, 8}, + 7: {9, 10}, + 8: {^7, ^4}, + 9: {0, ^9}, + 10: {^8, ^5}, +} + +// whiteDecodeTable represents Tables 2 and 3 for a white run. +// +// +=XXXXX +// b059 +-+ +// | | +=v1792 +// b096 | | +-+ +// | | | | +=v1984 +// b100 | | | +-+ +// | | | +=v2048 +// b094 | | +-+ +// | | | | +=v2112 +// b101 | | | | +-+ +// | | | | | +=v2176 +// b097 | | | +-+ +// | | | | +=v2240 +// b102 | | | +-+ +// | | | +=v2304 +// b085 | +-+ +// | | +=v1856 +// b098 | | +-+ +// | | | +=v1920 +// b095 | +-+ +// | | +=v2368 +// b103 | | +-+ +// | | | +=v2432 +// b099 | +-+ +// | | +=v2496 +// b104 | +-+ +// | +=v2560 +// b040 +-+ +// | | +=v0029 +// b060 | +-+ +// | +=v0030 +// b026 +-+ +// | | +=v0045 +// b061 | | +-+ +// | | | +=v0046 +// b041 | +-+ +// | +=v0022 +// b016 +-+ +// | | +=v0023 +// b042 | | +-+ +// | | | | +=v0047 +// b062 | | | +-+ +// | | | +=v0048 +// b027 | +-+ +// | +=v0013 +// b008 +-+ +// | | +=v0020 +// b043 | | +-+ +// | | | | +=v0033 +// b063 | | | +-+ +// | | | +=v0034 +// b028 | | +-+ +// | | | | +=v0035 +// b064 | | | | +-+ +// | | | | | +=v0036 +// b044 | | | +-+ +// | | | | +=v0037 +// b065 | | | +-+ +// | | | +=v0038 +// b017 | +-+ +// | | +=v0019 +// b045 | | +-+ +// | | | | +=v0031 +// b066 | | | +-+ +// | | | +=v0032 +// b029 | +-+ +// | +=v0001 +// b004 +-+ +// | | +=v0012 +// b030 | | +-+ +// | | | | +=v0053 +// b067 | | | | +-+ +// | | | | | +=v0054 +// b046 | | | +-+ +// | | | +=v0026 +// b018 | | +-+ +// | | | | +=v0039 +// b068 | | | | +-+ +// | | | | | +=v0040 +// b047 | | | | +-+ +// | | | | | | +=v0041 +// b069 | | | | | +-+ +// | | | | | +=v0042 +// b031 | | | +-+ +// | | | | +=v0043 +// b070 | | | | +-+ +// | | | | | +=v0044 +// b048 | | | +-+ +// | | | +=v0021 +// b009 | +-+ +// | | +=v0028 +// b049 | | +-+ +// | | | | +=v0061 +// b071 | | | +-+ +// | | | +=v0062 +// b032 | | +-+ +// | | | | +=v0063 +// b072 | | | | +-+ +// | | | | | +=v0000 +// b050 | | | +-+ +// | | | | +=v0320 +// b073 | | | +-+ +// | | | +=v0384 +// b019 | +-+ +// | +=v0010 +// b002 +-+ +// | | +=v0011 +// b020 | | +-+ +// | | | | +=v0027 +// b051 | | | | +-+ +// | | | | | | +=v0059 +// b074 | | | | | +-+ +// | | | | | +=v0060 +// b033 | | | +-+ +// | | | | +=v1472 +// b086 | | | | +-+ +// | | | | | +=v1536 +// b075 | | | | +-+ +// | | | | | | +=v1600 +// b087 | | | | | +-+ +// | | | | | +=v1728 +// b052 | | | +-+ +// | | | +=v0018 +// b010 | | +-+ +// | | | | +=v0024 +// b053 | | | | +-+ +// | | | | | | +=v0049 +// b076 | | | | | +-+ +// | | | | | +=v0050 +// b034 | | | | +-+ +// | | | | | | +=v0051 +// b077 | | | | | | +-+ +// | | | | | | | +=v0052 +// b054 | | | | | +-+ +// | | | | | +=v0025 +// b021 | | | +-+ +// | | | | +=v0055 +// b078 | | | | +-+ +// | | | | | +=v0056 +// b055 | | | | +-+ +// | | | | | | +=v0057 +// b079 | | | | | +-+ +// | | | | | +=v0058 +// b035 | | | +-+ +// | | | +=v0192 +// b005 | +-+ +// | | +=v1664 +// b036 | | +-+ +// | | | | +=v0448 +// b080 | | | | +-+ +// | | | | | +=v0512 +// b056 | | | +-+ +// | | | | +=v0704 +// b088 | | | | +-+ +// | | | | | +=v0768 +// b081 | | | +-+ +// | | | +=v0640 +// b022 | | +-+ +// | | | | +=v0576 +// b082 | | | | +-+ +// | | | | | | +=v0832 +// b089 | | | | | +-+ +// | | | | | +=v0896 +// b057 | | | | +-+ +// | | | | | | +=v0960 +// b090 | | | | | | +-+ +// | | | | | | | +=v1024 +// b083 | | | | | +-+ +// | | | | | | +=v1088 +// b091 | | | | | +-+ +// | | | | | +=v1152 +// b037 | | | +-+ +// | | | | +=v1216 +// b092 | | | | +-+ +// | | | | | +=v1280 +// b084 | | | | +-+ +// | | | | | | +=v1344 +// b093 | | | | | +-+ +// | | | | | +=v1408 +// b058 | | | +-+ +// | | | +=v0256 +// b011 | +-+ +// | +=v0002 +// b001 +-+ +// | +=v0003 +// b012 | +-+ +// | | | +=v0128 +// b023 | | +-+ +// | | +=v0008 +// b006 | +-+ +// | | | +=v0009 +// b024 | | | +-+ +// | | | | | +=v0016 +// b038 | | | | +-+ +// | | | | +=v0017 +// b013 | | +-+ +// | | +=v0004 +// b003 +-+ +// | +=v0005 +// b014 | +-+ +// | | | +=v0014 +// b039 | | | +-+ +// | | | | +=v0015 +// b025 | | +-+ +// | | +=v0064 +// b007 +-+ +// | +=v0006 +// b015 +-+ +// +=v0007 +var whiteDecodeTable = [...][2]int16{ + 0: {0, 0}, + 1: {2, 3}, + 2: {4, 5}, + 3: {6, 7}, + 4: {8, 9}, + 5: {10, 11}, + 6: {12, 13}, + 7: {14, 15}, + 8: {16, 17}, + 9: {18, 19}, + 10: {20, 21}, + 11: {22, ^2}, + 12: {^3, 23}, + 13: {24, ^4}, + 14: {^5, 25}, + 15: {^6, ^7}, + 16: {26, 27}, + 17: {28, 29}, + 18: {30, 31}, + 19: {32, ^10}, + 20: {^11, 33}, + 21: {34, 35}, + 22: {36, 37}, + 23: {^128, ^8}, + 24: {^9, 38}, + 25: {39, ^64}, + 26: {40, 41}, + 27: {42, ^13}, + 28: {43, 44}, + 29: {45, ^1}, + 30: {^12, 46}, + 31: {47, 48}, + 32: {49, 50}, + 33: {51, 52}, + 34: {53, 54}, + 35: {55, ^192}, + 36: {^1664, 56}, + 37: {57, 58}, + 38: {^16, ^17}, + 39: {^14, ^15}, + 40: {59, 60}, + 41: {61, ^22}, + 42: {^23, 62}, + 43: {^20, 63}, + 44: {64, 65}, + 45: {^19, 66}, + 46: {67, ^26}, + 47: {68, 69}, + 48: {70, ^21}, + 49: {^28, 71}, + 50: {72, 73}, + 51: {^27, 74}, + 52: {75, ^18}, + 53: {^24, 76}, + 54: {77, ^25}, + 55: {78, 79}, + 56: {80, 81}, + 57: {82, 83}, + 58: {84, ^256}, + 59: {0, 85}, + 60: {^29, ^30}, + 61: {^45, ^46}, + 62: {^47, ^48}, + 63: {^33, ^34}, + 64: {^35, ^36}, + 65: {^37, ^38}, + 66: {^31, ^32}, + 67: {^53, ^54}, + 68: {^39, ^40}, + 69: {^41, ^42}, + 70: {^43, ^44}, + 71: {^61, ^62}, + 72: {^63, ^0}, + 73: {^320, ^384}, + 74: {^59, ^60}, + 75: {86, 87}, + 76: {^49, ^50}, + 77: {^51, ^52}, + 78: {^55, ^56}, + 79: {^57, ^58}, + 80: {^448, ^512}, + 81: {88, ^640}, + 82: {^576, 89}, + 83: {90, 91}, + 84: {92, 93}, + 85: {94, 95}, + 86: {^1472, ^1536}, + 87: {^1600, ^1728}, + 88: {^704, ^768}, + 89: {^832, ^896}, + 90: {^960, ^1024}, + 91: {^1088, ^1152}, + 92: {^1216, ^1280}, + 93: {^1344, ^1408}, + 94: {96, 97}, + 95: {98, 99}, + 96: {^1792, 100}, + 97: {101, 102}, + 98: {^1856, ^1920}, + 99: {103, 104}, + 100: {^1984, ^2048}, + 101: {^2112, ^2176}, + 102: {^2240, ^2304}, + 103: {^2368, ^2432}, + 104: {^2496, ^2560}, +} + +// blackDecodeTable represents Tables 2 and 3 for a black run. +// +// +=XXXXX +// b017 +-+ +// | | +=v1792 +// b042 | | +-+ +// | | | | +=v1984 +// b063 | | | +-+ +// | | | +=v2048 +// b029 | | +-+ +// | | | | +=v2112 +// b064 | | | | +-+ +// | | | | | +=v2176 +// b043 | | | +-+ +// | | | | +=v2240 +// b065 | | | +-+ +// | | | +=v2304 +// b022 | +-+ +// | | +=v1856 +// b044 | | +-+ +// | | | +=v1920 +// b030 | +-+ +// | | +=v2368 +// b066 | | +-+ +// | | | +=v2432 +// b045 | +-+ +// | | +=v2496 +// b067 | +-+ +// | +=v2560 +// b013 +-+ +// | | +=v0018 +// b031 | | +-+ +// | | | | +=v0052 +// b068 | | | | +-+ +// | | | | | | +=v0640 +// b095 | | | | | +-+ +// | | | | | +=v0704 +// b046 | | | +-+ +// | | | | +=v0768 +// b096 | | | | +-+ +// | | | | | +=v0832 +// b069 | | | +-+ +// | | | +=v0055 +// b023 | | +-+ +// | | | | +=v0056 +// b070 | | | | +-+ +// | | | | | | +=v1280 +// b097 | | | | | +-+ +// | | | | | +=v1344 +// b047 | | | | +-+ +// | | | | | | +=v1408 +// b098 | | | | | | +-+ +// | | | | | | | +=v1472 +// b071 | | | | | +-+ +// | | | | | +=v0059 +// b032 | | | +-+ +// | | | | +=v0060 +// b072 | | | | +-+ +// | | | | | | +=v1536 +// b099 | | | | | +-+ +// | | | | | +=v1600 +// b048 | | | +-+ +// | | | +=v0024 +// b018 | +-+ +// | | +=v0025 +// b049 | | +-+ +// | | | | +=v1664 +// b100 | | | | +-+ +// | | | | | +=v1728 +// b073 | | | +-+ +// | | | +=v0320 +// b033 | | +-+ +// | | | | +=v0384 +// b074 | | | | +-+ +// | | | | | +=v0448 +// b050 | | | +-+ +// | | | | +=v0512 +// b101 | | | | +-+ +// | | | | | +=v0576 +// b075 | | | +-+ +// | | | +=v0053 +// b024 | +-+ +// | | +=v0054 +// b076 | | +-+ +// | | | | +=v0896 +// b102 | | | +-+ +// | | | +=v0960 +// b051 | | +-+ +// | | | | +=v1024 +// b103 | | | | +-+ +// | | | | | +=v1088 +// b077 | | | +-+ +// | | | | +=v1152 +// b104 | | | +-+ +// | | | +=v1216 +// b034 | +-+ +// | +=v0064 +// b010 +-+ +// | | +=v0013 +// b019 | | +-+ +// | | | | +=v0023 +// b052 | | | | +-+ +// | | | | | | +=v0050 +// b078 | | | | | +-+ +// | | | | | +=v0051 +// b035 | | | | +-+ +// | | | | | | +=v0044 +// b079 | | | | | | +-+ +// | | | | | | | +=v0045 +// b053 | | | | | +-+ +// | | | | | | +=v0046 +// b080 | | | | | +-+ +// | | | | | +=v0047 +// b025 | | | +-+ +// | | | | +=v0057 +// b081 | | | | +-+ +// | | | | | +=v0058 +// b054 | | | | +-+ +// | | | | | | +=v0061 +// b082 | | | | | +-+ +// | | | | | +=v0256 +// b036 | | | +-+ +// | | | +=v0016 +// b014 | +-+ +// | | +=v0017 +// b037 | | +-+ +// | | | | +=v0048 +// b083 | | | | +-+ +// | | | | | +=v0049 +// b055 | | | +-+ +// | | | | +=v0062 +// b084 | | | +-+ +// | | | +=v0063 +// b026 | | +-+ +// | | | | +=v0030 +// b085 | | | | +-+ +// | | | | | +=v0031 +// b056 | | | | +-+ +// | | | | | | +=v0032 +// b086 | | | | | +-+ +// | | | | | +=v0033 +// b038 | | | +-+ +// | | | | +=v0040 +// b087 | | | | +-+ +// | | | | | +=v0041 +// b057 | | | +-+ +// | | | +=v0022 +// b020 | +-+ +// | +=v0014 +// b008 +-+ +// | | +=v0010 +// b015 | | +-+ +// | | | +=v0011 +// b011 | +-+ +// | | +=v0015 +// b027 | | +-+ +// | | | | +=v0128 +// b088 | | | | +-+ +// | | | | | +=v0192 +// b058 | | | | +-+ +// | | | | | | +=v0026 +// b089 | | | | | +-+ +// | | | | | +=v0027 +// b039 | | | +-+ +// | | | | +=v0028 +// b090 | | | | +-+ +// | | | | | +=v0029 +// b059 | | | +-+ +// | | | +=v0019 +// b021 | | +-+ +// | | | | +=v0020 +// b060 | | | | +-+ +// | | | | | | +=v0034 +// b091 | | | | | +-+ +// | | | | | +=v0035 +// b040 | | | | +-+ +// | | | | | | +=v0036 +// b092 | | | | | | +-+ +// | | | | | | | +=v0037 +// b061 | | | | | +-+ +// | | | | | | +=v0038 +// b093 | | | | | +-+ +// | | | | | +=v0039 +// b028 | | | +-+ +// | | | | +=v0021 +// b062 | | | | +-+ +// | | | | | | +=v0042 +// b094 | | | | | +-+ +// | | | | | +=v0043 +// b041 | | | +-+ +// | | | +=v0000 +// b016 | +-+ +// | +=v0012 +// b006 +-+ +// | | +=v0009 +// b012 | | +-+ +// | | | +=v0008 +// b009 | +-+ +// | +=v0007 +// b004 +-+ +// | | +=v0006 +// b007 | +-+ +// | +=v0005 +// b002 +-+ +// | | +=v0001 +// b005 | +-+ +// | +=v0004 +// b001 +-+ +// | +=v0003 +// b003 +-+ +// +=v0002 +var blackDecodeTable = [...][2]int16{ + 0: {0, 0}, + 1: {2, 3}, + 2: {4, 5}, + 3: {^3, ^2}, + 4: {6, 7}, + 5: {^1, ^4}, + 6: {8, 9}, + 7: {^6, ^5}, + 8: {10, 11}, + 9: {12, ^7}, + 10: {13, 14}, + 11: {15, 16}, + 12: {^9, ^8}, + 13: {17, 18}, + 14: {19, 20}, + 15: {^10, ^11}, + 16: {21, ^12}, + 17: {0, 22}, + 18: {23, 24}, + 19: {^13, 25}, + 20: {26, ^14}, + 21: {27, 28}, + 22: {29, 30}, + 23: {31, 32}, + 24: {33, 34}, + 25: {35, 36}, + 26: {37, 38}, + 27: {^15, 39}, + 28: {40, 41}, + 29: {42, 43}, + 30: {44, 45}, + 31: {^18, 46}, + 32: {47, 48}, + 33: {49, 50}, + 34: {51, ^64}, + 35: {52, 53}, + 36: {54, ^16}, + 37: {^17, 55}, + 38: {56, 57}, + 39: {58, 59}, + 40: {60, 61}, + 41: {62, ^0}, + 42: {^1792, 63}, + 43: {64, 65}, + 44: {^1856, ^1920}, + 45: {66, 67}, + 46: {68, 69}, + 47: {70, 71}, + 48: {72, ^24}, + 49: {^25, 73}, + 50: {74, 75}, + 51: {76, 77}, + 52: {^23, 78}, + 53: {79, 80}, + 54: {81, 82}, + 55: {83, 84}, + 56: {85, 86}, + 57: {87, ^22}, + 58: {88, 89}, + 59: {90, ^19}, + 60: {^20, 91}, + 61: {92, 93}, + 62: {^21, 94}, + 63: {^1984, ^2048}, + 64: {^2112, ^2176}, + 65: {^2240, ^2304}, + 66: {^2368, ^2432}, + 67: {^2496, ^2560}, + 68: {^52, 95}, + 69: {96, ^55}, + 70: {^56, 97}, + 71: {98, ^59}, + 72: {^60, 99}, + 73: {100, ^320}, + 74: {^384, ^448}, + 75: {101, ^53}, + 76: {^54, 102}, + 77: {103, 104}, + 78: {^50, ^51}, + 79: {^44, ^45}, + 80: {^46, ^47}, + 81: {^57, ^58}, + 82: {^61, ^256}, + 83: {^48, ^49}, + 84: {^62, ^63}, + 85: {^30, ^31}, + 86: {^32, ^33}, + 87: {^40, ^41}, + 88: {^128, ^192}, + 89: {^26, ^27}, + 90: {^28, ^29}, + 91: {^34, ^35}, + 92: {^36, ^37}, + 93: {^38, ^39}, + 94: {^42, ^43}, + 95: {^640, ^704}, + 96: {^768, ^832}, + 97: {^1280, ^1344}, + 98: {^1408, ^1472}, + 99: {^1536, ^1600}, + 100: {^1664, ^1728}, + 101: {^512, ^576}, + 102: {^896, ^960}, + 103: {^1024, ^1088}, + 104: {^1152, ^1216}, +} + +const maxCodeLength = 13 + +// Each encodeTable is represented by an array of bitStrings. + +// bitString is a pair of uint32 values representing a bit code. +// The nBits low bits of bits make up the actual bit code. +// Eg. bitString{0x0004, 8} represents the bitcode "00000100". +type bitString struct { + bits uint32 + nBits uint32 +} + +// modeEncodeTable represents Table 1 and the End-of-Line code. +var modeEncodeTable = [...]bitString{ + 0: {0x0001, 4}, // "0001" + 1: {0x0001, 3}, // "001" + 2: {0x0001, 1}, // "1" + 3: {0x0003, 3}, // "011" + 4: {0x0003, 6}, // "000011" + 5: {0x0003, 7}, // "0000011" + 6: {0x0002, 3}, // "010" + 7: {0x0002, 6}, // "000010" + 8: {0x0002, 7}, // "0000010" + 9: {0x0001, 7}, // "0000001" +} + +// whiteEncodeTable2 represents Table 2 for a white run. +var whiteEncodeTable2 = [...]bitString{ + 0: {0x0035, 8}, // "00110101" + 1: {0x0007, 6}, // "000111" + 2: {0x0007, 4}, // "0111" + 3: {0x0008, 4}, // "1000" + 4: {0x000b, 4}, // "1011" + 5: {0x000c, 4}, // "1100" + 6: {0x000e, 4}, // "1110" + 7: {0x000f, 4}, // "1111" + 8: {0x0013, 5}, // "10011" + 9: {0x0014, 5}, // "10100" + 10: {0x0007, 5}, // "00111" + 11: {0x0008, 5}, // "01000" + 12: {0x0008, 6}, // "001000" + 13: {0x0003, 6}, // "000011" + 14: {0x0034, 6}, // "110100" + 15: {0x0035, 6}, // "110101" + 16: {0x002a, 6}, // "101010" + 17: {0x002b, 6}, // "101011" + 18: {0x0027, 7}, // "0100111" + 19: {0x000c, 7}, // "0001100" + 20: {0x0008, 7}, // "0001000" + 21: {0x0017, 7}, // "0010111" + 22: {0x0003, 7}, // "0000011" + 23: {0x0004, 7}, // "0000100" + 24: {0x0028, 7}, // "0101000" + 25: {0x002b, 7}, // "0101011" + 26: {0x0013, 7}, // "0010011" + 27: {0x0024, 7}, // "0100100" + 28: {0x0018, 7}, // "0011000" + 29: {0x0002, 8}, // "00000010" + 30: {0x0003, 8}, // "00000011" + 31: {0x001a, 8}, // "00011010" + 32: {0x001b, 8}, // "00011011" + 33: {0x0012, 8}, // "00010010" + 34: {0x0013, 8}, // "00010011" + 35: {0x0014, 8}, // "00010100" + 36: {0x0015, 8}, // "00010101" + 37: {0x0016, 8}, // "00010110" + 38: {0x0017, 8}, // "00010111" + 39: {0x0028, 8}, // "00101000" + 40: {0x0029, 8}, // "00101001" + 41: {0x002a, 8}, // "00101010" + 42: {0x002b, 8}, // "00101011" + 43: {0x002c, 8}, // "00101100" + 44: {0x002d, 8}, // "00101101" + 45: {0x0004, 8}, // "00000100" + 46: {0x0005, 8}, // "00000101" + 47: {0x000a, 8}, // "00001010" + 48: {0x000b, 8}, // "00001011" + 49: {0x0052, 8}, // "01010010" + 50: {0x0053, 8}, // "01010011" + 51: {0x0054, 8}, // "01010100" + 52: {0x0055, 8}, // "01010101" + 53: {0x0024, 8}, // "00100100" + 54: {0x0025, 8}, // "00100101" + 55: {0x0058, 8}, // "01011000" + 56: {0x0059, 8}, // "01011001" + 57: {0x005a, 8}, // "01011010" + 58: {0x005b, 8}, // "01011011" + 59: {0x004a, 8}, // "01001010" + 60: {0x004b, 8}, // "01001011" + 61: {0x0032, 8}, // "00110010" + 62: {0x0033, 8}, // "00110011" + 63: {0x0034, 8}, // "00110100" +} + +// whiteEncodeTable3 represents Table 3 for a white run. +var whiteEncodeTable3 = [...]bitString{ + 0: {0x001b, 5}, // "11011" + 1: {0x0012, 5}, // "10010" + 2: {0x0017, 6}, // "010111" + 3: {0x0037, 7}, // "0110111" + 4: {0x0036, 8}, // "00110110" + 5: {0x0037, 8}, // "00110111" + 6: {0x0064, 8}, // "01100100" + 7: {0x0065, 8}, // "01100101" + 8: {0x0068, 8}, // "01101000" + 9: {0x0067, 8}, // "01100111" + 10: {0x00cc, 9}, // "011001100" + 11: {0x00cd, 9}, // "011001101" + 12: {0x00d2, 9}, // "011010010" + 13: {0x00d3, 9}, // "011010011" + 14: {0x00d4, 9}, // "011010100" + 15: {0x00d5, 9}, // "011010101" + 16: {0x00d6, 9}, // "011010110" + 17: {0x00d7, 9}, // "011010111" + 18: {0x00d8, 9}, // "011011000" + 19: {0x00d9, 9}, // "011011001" + 20: {0x00da, 9}, // "011011010" + 21: {0x00db, 9}, // "011011011" + 22: {0x0098, 9}, // "010011000" + 23: {0x0099, 9}, // "010011001" + 24: {0x009a, 9}, // "010011010" + 25: {0x0018, 6}, // "011000" + 26: {0x009b, 9}, // "010011011" + 27: {0x0008, 11}, // "00000001000" + 28: {0x000c, 11}, // "00000001100" + 29: {0x000d, 11}, // "00000001101" + 30: {0x0012, 12}, // "000000010010" + 31: {0x0013, 12}, // "000000010011" + 32: {0x0014, 12}, // "000000010100" + 33: {0x0015, 12}, // "000000010101" + 34: {0x0016, 12}, // "000000010110" + 35: {0x0017, 12}, // "000000010111" + 36: {0x001c, 12}, // "000000011100" + 37: {0x001d, 12}, // "000000011101" + 38: {0x001e, 12}, // "000000011110" + 39: {0x001f, 12}, // "000000011111" +} + +// blackEncodeTable2 represents Table 2 for a black run. +var blackEncodeTable2 = [...]bitString{ + 0: {0x0037, 10}, // "0000110111" + 1: {0x0002, 3}, // "010" + 2: {0x0003, 2}, // "11" + 3: {0x0002, 2}, // "10" + 4: {0x0003, 3}, // "011" + 5: {0x0003, 4}, // "0011" + 6: {0x0002, 4}, // "0010" + 7: {0x0003, 5}, // "00011" + 8: {0x0005, 6}, // "000101" + 9: {0x0004, 6}, // "000100" + 10: {0x0004, 7}, // "0000100" + 11: {0x0005, 7}, // "0000101" + 12: {0x0007, 7}, // "0000111" + 13: {0x0004, 8}, // "00000100" + 14: {0x0007, 8}, // "00000111" + 15: {0x0018, 9}, // "000011000" + 16: {0x0017, 10}, // "0000010111" + 17: {0x0018, 10}, // "0000011000" + 18: {0x0008, 10}, // "0000001000" + 19: {0x0067, 11}, // "00001100111" + 20: {0x0068, 11}, // "00001101000" + 21: {0x006c, 11}, // "00001101100" + 22: {0x0037, 11}, // "00000110111" + 23: {0x0028, 11}, // "00000101000" + 24: {0x0017, 11}, // "00000010111" + 25: {0x0018, 11}, // "00000011000" + 26: {0x00ca, 12}, // "000011001010" + 27: {0x00cb, 12}, // "000011001011" + 28: {0x00cc, 12}, // "000011001100" + 29: {0x00cd, 12}, // "000011001101" + 30: {0x0068, 12}, // "000001101000" + 31: {0x0069, 12}, // "000001101001" + 32: {0x006a, 12}, // "000001101010" + 33: {0x006b, 12}, // "000001101011" + 34: {0x00d2, 12}, // "000011010010" + 35: {0x00d3, 12}, // "000011010011" + 36: {0x00d4, 12}, // "000011010100" + 37: {0x00d5, 12}, // "000011010101" + 38: {0x00d6, 12}, // "000011010110" + 39: {0x00d7, 12}, // "000011010111" + 40: {0x006c, 12}, // "000001101100" + 41: {0x006d, 12}, // "000001101101" + 42: {0x00da, 12}, // "000011011010" + 43: {0x00db, 12}, // "000011011011" + 44: {0x0054, 12}, // "000001010100" + 45: {0x0055, 12}, // "000001010101" + 46: {0x0056, 12}, // "000001010110" + 47: {0x0057, 12}, // "000001010111" + 48: {0x0064, 12}, // "000001100100" + 49: {0x0065, 12}, // "000001100101" + 50: {0x0052, 12}, // "000001010010" + 51: {0x0053, 12}, // "000001010011" + 52: {0x0024, 12}, // "000000100100" + 53: {0x0037, 12}, // "000000110111" + 54: {0x0038, 12}, // "000000111000" + 55: {0x0027, 12}, // "000000100111" + 56: {0x0028, 12}, // "000000101000" + 57: {0x0058, 12}, // "000001011000" + 58: {0x0059, 12}, // "000001011001" + 59: {0x002b, 12}, // "000000101011" + 60: {0x002c, 12}, // "000000101100" + 61: {0x005a, 12}, // "000001011010" + 62: {0x0066, 12}, // "000001100110" + 63: {0x0067, 12}, // "000001100111" +} + +// blackEncodeTable3 represents Table 3 for a black run. +var blackEncodeTable3 = [...]bitString{ + 0: {0x000f, 10}, // "0000001111" + 1: {0x00c8, 12}, // "000011001000" + 2: {0x00c9, 12}, // "000011001001" + 3: {0x005b, 12}, // "000001011011" + 4: {0x0033, 12}, // "000000110011" + 5: {0x0034, 12}, // "000000110100" + 6: {0x0035, 12}, // "000000110101" + 7: {0x006c, 13}, // "0000001101100" + 8: {0x006d, 13}, // "0000001101101" + 9: {0x004a, 13}, // "0000001001010" + 10: {0x004b, 13}, // "0000001001011" + 11: {0x004c, 13}, // "0000001001100" + 12: {0x004d, 13}, // "0000001001101" + 13: {0x0072, 13}, // "0000001110010" + 14: {0x0073, 13}, // "0000001110011" + 15: {0x0074, 13}, // "0000001110100" + 16: {0x0075, 13}, // "0000001110101" + 17: {0x0076, 13}, // "0000001110110" + 18: {0x0077, 13}, // "0000001110111" + 19: {0x0052, 13}, // "0000001010010" + 20: {0x0053, 13}, // "0000001010011" + 21: {0x0054, 13}, // "0000001010100" + 22: {0x0055, 13}, // "0000001010101" + 23: {0x005a, 13}, // "0000001011010" + 24: {0x005b, 13}, // "0000001011011" + 25: {0x0064, 13}, // "0000001100100" + 26: {0x0065, 13}, // "0000001100101" + 27: {0x0008, 11}, // "00000001000" + 28: {0x000c, 11}, // "00000001100" + 29: {0x000d, 11}, // "00000001101" + 30: {0x0012, 12}, // "000000010010" + 31: {0x0013, 12}, // "000000010011" + 32: {0x0014, 12}, // "000000010100" + 33: {0x0015, 12}, // "000000010101" + 34: {0x0016, 12}, // "000000010110" + 35: {0x0017, 12}, // "000000010111" + 36: {0x001c, 12}, // "000000011100" + 37: {0x001d, 12}, // "000000011101" + 38: {0x001e, 12}, // "000000011110" + 39: {0x001f, 12}, // "000000011111" +} + +// COPY PASTE table.go BEGIN + +const ( + modePass = iota // Pass + modeH // Horizontal + modeV0 // Vertical-0 + modeVR1 // Vertical-Right-1 + modeVR2 // Vertical-Right-2 + modeVR3 // Vertical-Right-3 + modeVL1 // Vertical-Left-1 + modeVL2 // Vertical-Left-2 + modeVL3 // Vertical-Left-3 + modeExt // Extension +) + +// COPY PASTE table.go END diff --git a/vendor/golang.org/x/image/ccitt/writer.go b/vendor/golang.org/x/image/ccitt/writer.go new file mode 100644 index 00000000..87130ab0 --- /dev/null +++ b/vendor/golang.org/x/image/ccitt/writer.go @@ -0,0 +1,102 @@ +// Copyright 2019 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. + +package ccitt + +import ( + "encoding/binary" + "io" +) + +type bitWriter struct { + w io.Writer + + // order is whether to process w's bytes LSB first or MSB first. + order Order + + // The high nBits bits of the bits field hold encoded bits to be written to w. + bits uint64 + nBits uint32 + + // bytes[:bw] holds encoded bytes not yet written to w. + // Overflow protection is ensured by using a multiple of 8 as bytes length. + bw uint32 + bytes [1024]uint8 +} + +// flushBits copies 64 bits from b.bits to b.bytes. If b.bytes is then full, it +// is written to b.w. +func (b *bitWriter) flushBits() error { + binary.BigEndian.PutUint64(b.bytes[b.bw:], b.bits) + b.bits = 0 + b.nBits = 0 + b.bw += 8 + if b.bw < uint32(len(b.bytes)) { + return nil + } + b.bw = 0 + if b.order != MSB { + reverseBitsWithinBytes(b.bytes[:]) + } + _, err := b.w.Write(b.bytes[:]) + return err +} + +// close finalizes a bitcode stream by writing any +// pending bits to bitWriter's underlying io.Writer. +func (b *bitWriter) close() error { + // Write any encoded bits to bytes. + if b.nBits > 0 { + binary.BigEndian.PutUint64(b.bytes[b.bw:], b.bits) + b.bw += (b.nBits + 7) >> 3 + } + + if b.order != MSB { + reverseBitsWithinBytes(b.bytes[:b.bw]) + } + + // Write b.bw bytes to b.w. + _, err := b.w.Write(b.bytes[:b.bw]) + return err +} + +// alignToByteBoundary rounds b.nBits up to a multiple of 8. +// If all 64 bits are used, flush them to bitWriter's bytes. +func (b *bitWriter) alignToByteBoundary() error { + if b.nBits = (b.nBits + 7) &^ 7; b.nBits == 64 { + return b.flushBits() + } + return nil +} + +// writeCode writes a variable length bitcode to b's underlying io.Writer. +func (b *bitWriter) writeCode(bs bitString) error { + bits := bs.bits + nBits := bs.nBits + if 64-b.nBits >= nBits { + // b.bits has sufficient room for storing nBits bits. + b.bits |= uint64(bits) << (64 - nBits - b.nBits) + b.nBits += nBits + if b.nBits == 64 { + return b.flushBits() + } + return nil + } + + // Number of leading bits that fill b.bits. + i := 64 - b.nBits + + // Fill b.bits then flush and write remaining bits. + b.bits |= uint64(bits) >> (nBits - i) + b.nBits = 64 + + if err := b.flushBits(); err != nil { + return err + } + + nBits -= i + b.bits = uint64(bits) << (64 - nBits) + b.nBits = nBits + return nil +} diff --git a/vendor/golang.org/x/image/riff/riff.go b/vendor/golang.org/x/image/riff/riff.go new file mode 100644 index 00000000..38dc0e56 --- /dev/null +++ b/vendor/golang.org/x/image/riff/riff.go @@ -0,0 +1,193 @@ +// Copyright 2014 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. + +// Package riff implements the Resource Interchange File Format, used by media +// formats such as AVI, WAVE and WEBP. +// +// A RIFF stream contains a sequence of chunks. Each chunk consists of an 8-byte +// header (containing a 4-byte chunk type and a 4-byte chunk length), the chunk +// data (presented as an io.Reader), and some padding bytes. +// +// A detailed description of the format is at +// http://www.tactilemedia.com/info/MCI_Control_Info.html +package riff // import "golang.org/x/image/riff" + +import ( + "errors" + "io" + "io/ioutil" + "math" +) + +var ( + errMissingPaddingByte = errors.New("riff: missing padding byte") + errMissingRIFFChunkHeader = errors.New("riff: missing RIFF chunk header") + errListSubchunkTooLong = errors.New("riff: list subchunk too long") + errShortChunkData = errors.New("riff: short chunk data") + errShortChunkHeader = errors.New("riff: short chunk header") + errStaleReader = errors.New("riff: stale reader") +) + +// u32 decodes the first four bytes of b as a little-endian integer. +func u32(b []byte) uint32 { + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +const chunkHeaderSize = 8 + +// FourCC is a four character code. +type FourCC [4]byte + +// LIST is the "LIST" FourCC. +var LIST = FourCC{'L', 'I', 'S', 'T'} + +// NewReader returns the RIFF stream's form type, such as "AVI " or "WAVE", and +// its chunks as a *Reader. +func NewReader(r io.Reader) (formType FourCC, data *Reader, err error) { + var buf [chunkHeaderSize]byte + if _, err := io.ReadFull(r, buf[:]); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = errMissingRIFFChunkHeader + } + return FourCC{}, nil, err + } + if buf[0] != 'R' || buf[1] != 'I' || buf[2] != 'F' || buf[3] != 'F' { + return FourCC{}, nil, errMissingRIFFChunkHeader + } + return NewListReader(u32(buf[4:]), r) +} + +// NewListReader returns a LIST chunk's list type, such as "movi" or "wavl", +// and its chunks as a *Reader. +func NewListReader(chunkLen uint32, chunkData io.Reader) (listType FourCC, data *Reader, err error) { + if chunkLen < 4 { + return FourCC{}, nil, errShortChunkData + } + z := &Reader{r: chunkData} + if _, err := io.ReadFull(chunkData, z.buf[:4]); err != nil { + if err == io.EOF || err == io.ErrUnexpectedEOF { + err = errShortChunkData + } + return FourCC{}, nil, err + } + z.totalLen = chunkLen - 4 + return FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]}, z, nil +} + +// Reader reads chunks from an underlying io.Reader. +type Reader struct { + r io.Reader + err error + + totalLen uint32 + chunkLen uint32 + + chunkReader *chunkReader + buf [chunkHeaderSize]byte + padded bool +} + +// Next returns the next chunk's ID, length and data. It returns io.EOF if there +// are no more chunks. The io.Reader returned becomes stale after the next Next +// call, and should no longer be used. +// +// It is valid to call Next even if all of the previous chunk's data has not +// been read. +func (z *Reader) Next() (chunkID FourCC, chunkLen uint32, chunkData io.Reader, err error) { + if z.err != nil { + return FourCC{}, 0, nil, z.err + } + + // Drain the rest of the previous chunk. + if z.chunkLen != 0 { + want := z.chunkLen + var got int64 + got, z.err = io.Copy(ioutil.Discard, z.chunkReader) + if z.err == nil && uint32(got) != want { + z.err = errShortChunkData + } + if z.err != nil { + return FourCC{}, 0, nil, z.err + } + } + z.chunkReader = nil + if z.padded { + if z.totalLen == 0 { + z.err = errListSubchunkTooLong + return FourCC{}, 0, nil, z.err + } + z.totalLen-- + _, z.err = io.ReadFull(z.r, z.buf[:1]) + if z.err != nil { + if z.err == io.EOF { + z.err = errMissingPaddingByte + } + return FourCC{}, 0, nil, z.err + } + } + + // We are done if we have no more data. + if z.totalLen == 0 { + z.err = io.EOF + return FourCC{}, 0, nil, z.err + } + + // Read the next chunk header. + if z.totalLen < chunkHeaderSize { + z.err = errShortChunkHeader + return FourCC{}, 0, nil, z.err + } + z.totalLen -= chunkHeaderSize + if _, z.err = io.ReadFull(z.r, z.buf[:chunkHeaderSize]); z.err != nil { + if z.err == io.EOF || z.err == io.ErrUnexpectedEOF { + z.err = errShortChunkHeader + } + return FourCC{}, 0, nil, z.err + } + chunkID = FourCC{z.buf[0], z.buf[1], z.buf[2], z.buf[3]} + z.chunkLen = u32(z.buf[4:]) + if z.chunkLen > z.totalLen { + z.err = errListSubchunkTooLong + return FourCC{}, 0, nil, z.err + } + z.padded = z.chunkLen&1 == 1 + z.chunkReader = &chunkReader{z} + return chunkID, z.chunkLen, z.chunkReader, nil +} + +type chunkReader struct { + z *Reader +} + +func (c *chunkReader) Read(p []byte) (int, error) { + if c != c.z.chunkReader { + return 0, errStaleReader + } + z := c.z + if z.err != nil { + if z.err == io.EOF { + return 0, errStaleReader + } + return 0, z.err + } + + n := int(z.chunkLen) + if n == 0 { + return 0, io.EOF + } + if n < 0 { + // Converting uint32 to int overflowed. + n = math.MaxInt32 + } + if n > len(p) { + n = len(p) + } + n, err := z.r.Read(p[:n]) + z.totalLen -= uint32(n) + z.chunkLen -= uint32(n) + if err != io.EOF { + z.err = err + } + return n, err +} diff --git a/vendor/golang.org/x/image/vp8/decode.go b/vendor/golang.org/x/image/vp8/decode.go new file mode 100644 index 00000000..2aa9fee0 --- /dev/null +++ b/vendor/golang.org/x/image/vp8/decode.go @@ -0,0 +1,403 @@ +// Copyright 2011 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. + +// Package vp8 implements a decoder for the VP8 lossy image format. +// +// The VP8 specification is RFC 6386. +package vp8 // import "golang.org/x/image/vp8" + +// This file implements the top-level decoding algorithm. + +import ( + "errors" + "image" + "io" +) + +// limitReader wraps an io.Reader to read at most n bytes from it. +type limitReader struct { + r io.Reader + n int +} + +// ReadFull reads exactly len(p) bytes into p. +func (r *limitReader) ReadFull(p []byte) error { + if len(p) > r.n { + return io.ErrUnexpectedEOF + } + n, err := io.ReadFull(r.r, p) + r.n -= n + return err +} + +// FrameHeader is a frame header, as specified in section 9.1. +type FrameHeader struct { + KeyFrame bool + VersionNumber uint8 + ShowFrame bool + FirstPartitionLen uint32 + Width int + Height int + XScale uint8 + YScale uint8 +} + +const ( + nSegment = 4 + nSegmentProb = 3 +) + +// segmentHeader holds segment-related header information. +type segmentHeader struct { + useSegment bool + updateMap bool + relativeDelta bool + quantizer [nSegment]int8 + filterStrength [nSegment]int8 + prob [nSegmentProb]uint8 +} + +const ( + nRefLFDelta = 4 + nModeLFDelta = 4 +) + +// filterHeader holds filter-related header information. +type filterHeader struct { + simple bool + level int8 + sharpness uint8 + useLFDelta bool + refLFDelta [nRefLFDelta]int8 + modeLFDelta [nModeLFDelta]int8 + perSegmentLevel [nSegment]int8 +} + +// mb is the per-macroblock decode state. A decoder maintains mbw+1 of these +// as it is decoding macroblocks left-to-right and top-to-bottom: mbw for the +// macroblocks in the row above, and one for the macroblock to the left. +type mb struct { + // pred is the predictor mode for the 4 bottom or right 4x4 luma regions. + pred [4]uint8 + // nzMask is a mask of 8 bits: 4 for the bottom or right 4x4 luma regions, + // and 2 + 2 for the bottom or right 4x4 chroma regions. A 1 bit indicates + // that region has non-zero coefficients. + nzMask uint8 + // nzY16 is a 0/1 value that is 1 if the macroblock used Y16 prediction and + // had non-zero coefficients. + nzY16 uint8 +} + +// Decoder decodes VP8 bitstreams into frames. Decoding one frame consists of +// calling Init, DecodeFrameHeader and then DecodeFrame in that order. +// A Decoder can be re-used to decode multiple frames. +type Decoder struct { + // r is the input bitsream. + r limitReader + // scratch is a scratch buffer. + scratch [8]byte + // img is the YCbCr image to decode into. + img *image.YCbCr + // mbw and mbh are the number of 16x16 macroblocks wide and high the image is. + mbw, mbh int + // frameHeader is the frame header. When decoding multiple frames, + // frames that aren't key frames will inherit the Width, Height, + // XScale and YScale of the most recent key frame. + frameHeader FrameHeader + // Other headers. + segmentHeader segmentHeader + filterHeader filterHeader + // The image data is divided into a number of independent partitions. + // There is 1 "first partition" and between 1 and 8 "other partitions" + // for coefficient data. + fp partition + op [8]partition + nOP int + // Quantization factors. + quant [nSegment]quant + // DCT/WHT coefficient decoding probabilities. + tokenProb [nPlane][nBand][nContext][nProb]uint8 + useSkipProb bool + skipProb uint8 + // Loop filter parameters. + filterParams [nSegment][2]filterParam + perMBFilterParams []filterParam + + // The eight fields below relate to the current macroblock being decoded. + // + // Segment-based adjustments. + segment int + // Per-macroblock state for the macroblock immediately left of and those + // macroblocks immediately above the current macroblock. + leftMB mb + upMB []mb + // Bitmasks for which 4x4 regions of coeff contain non-zero coefficients. + nzDCMask, nzACMask uint32 + // Predictor modes. + usePredY16 bool // The libwebp C code calls this !is_i4x4_. + predY16 uint8 + predC8 uint8 + predY4 [4][4]uint8 + + // The two fields below form a workspace for reconstructing a macroblock. + // Their specific sizes are documented in reconstruct.go. + coeff [1*16*16 + 2*8*8 + 1*4*4]int16 + ybr [1 + 16 + 1 + 8][32]uint8 +} + +// NewDecoder returns a new Decoder. +func NewDecoder() *Decoder { + return &Decoder{} +} + +// Init initializes the decoder to read at most n bytes from r. +func (d *Decoder) Init(r io.Reader, n int) { + d.r = limitReader{r, n} +} + +// DecodeFrameHeader decodes the frame header. +func (d *Decoder) DecodeFrameHeader() (fh FrameHeader, err error) { + // All frame headers are at least 3 bytes long. + b := d.scratch[:3] + if err = d.r.ReadFull(b); err != nil { + return + } + d.frameHeader.KeyFrame = (b[0] & 1) == 0 + d.frameHeader.VersionNumber = (b[0] >> 1) & 7 + d.frameHeader.ShowFrame = (b[0]>>4)&1 == 1 + d.frameHeader.FirstPartitionLen = uint32(b[0])>>5 | uint32(b[1])<<3 | uint32(b[2])<<11 + if !d.frameHeader.KeyFrame { + return d.frameHeader, nil + } + // Frame headers for key frames are an additional 7 bytes long. + b = d.scratch[:7] + if err = d.r.ReadFull(b); err != nil { + return + } + // Check the magic sync code. + if b[0] != 0x9d || b[1] != 0x01 || b[2] != 0x2a { + err = errors.New("vp8: invalid format") + return + } + d.frameHeader.Width = int(b[4]&0x3f)<<8 | int(b[3]) + d.frameHeader.Height = int(b[6]&0x3f)<<8 | int(b[5]) + d.frameHeader.XScale = b[4] >> 6 + d.frameHeader.YScale = b[6] >> 6 + d.mbw = (d.frameHeader.Width + 0x0f) >> 4 + d.mbh = (d.frameHeader.Height + 0x0f) >> 4 + d.segmentHeader = segmentHeader{ + prob: [3]uint8{0xff, 0xff, 0xff}, + } + d.tokenProb = defaultTokenProb + d.segment = 0 + return d.frameHeader, nil +} + +// ensureImg ensures that d.img is large enough to hold the decoded frame. +func (d *Decoder) ensureImg() { + if d.img != nil { + p0, p1 := d.img.Rect.Min, d.img.Rect.Max + if p0.X == 0 && p0.Y == 0 && p1.X >= 16*d.mbw && p1.Y >= 16*d.mbh { + return + } + } + m := image.NewYCbCr(image.Rect(0, 0, 16*d.mbw, 16*d.mbh), image.YCbCrSubsampleRatio420) + d.img = m.SubImage(image.Rect(0, 0, d.frameHeader.Width, d.frameHeader.Height)).(*image.YCbCr) + d.perMBFilterParams = make([]filterParam, d.mbw*d.mbh) + d.upMB = make([]mb, d.mbw) +} + +// parseSegmentHeader parses the segment header, as specified in section 9.3. +func (d *Decoder) parseSegmentHeader() { + d.segmentHeader.useSegment = d.fp.readBit(uniformProb) + if !d.segmentHeader.useSegment { + d.segmentHeader.updateMap = false + return + } + d.segmentHeader.updateMap = d.fp.readBit(uniformProb) + if d.fp.readBit(uniformProb) { + d.segmentHeader.relativeDelta = !d.fp.readBit(uniformProb) + for i := range d.segmentHeader.quantizer { + d.segmentHeader.quantizer[i] = int8(d.fp.readOptionalInt(uniformProb, 7)) + } + for i := range d.segmentHeader.filterStrength { + d.segmentHeader.filterStrength[i] = int8(d.fp.readOptionalInt(uniformProb, 6)) + } + } + if !d.segmentHeader.updateMap { + return + } + for i := range d.segmentHeader.prob { + if d.fp.readBit(uniformProb) { + d.segmentHeader.prob[i] = uint8(d.fp.readUint(uniformProb, 8)) + } else { + d.segmentHeader.prob[i] = 0xff + } + } +} + +// parseFilterHeader parses the filter header, as specified in section 9.4. +func (d *Decoder) parseFilterHeader() { + d.filterHeader.simple = d.fp.readBit(uniformProb) + d.filterHeader.level = int8(d.fp.readUint(uniformProb, 6)) + d.filterHeader.sharpness = uint8(d.fp.readUint(uniformProb, 3)) + d.filterHeader.useLFDelta = d.fp.readBit(uniformProb) + if d.filterHeader.useLFDelta && d.fp.readBit(uniformProb) { + for i := range d.filterHeader.refLFDelta { + d.filterHeader.refLFDelta[i] = int8(d.fp.readOptionalInt(uniformProb, 6)) + } + for i := range d.filterHeader.modeLFDelta { + d.filterHeader.modeLFDelta[i] = int8(d.fp.readOptionalInt(uniformProb, 6)) + } + } + if d.filterHeader.level == 0 { + return + } + if d.segmentHeader.useSegment { + for i := range d.filterHeader.perSegmentLevel { + strength := d.segmentHeader.filterStrength[i] + if d.segmentHeader.relativeDelta { + strength += d.filterHeader.level + } + d.filterHeader.perSegmentLevel[i] = strength + } + } else { + d.filterHeader.perSegmentLevel[0] = d.filterHeader.level + } + d.computeFilterParams() +} + +// parseOtherPartitions parses the other partitions, as specified in section 9.5. +func (d *Decoder) parseOtherPartitions() error { + const maxNOP = 1 << 3 + var partLens [maxNOP]int + d.nOP = 1 << d.fp.readUint(uniformProb, 2) + + // The final partition length is implied by the remaining chunk data + // (d.r.n) and the other d.nOP-1 partition lengths. Those d.nOP-1 partition + // lengths are stored as 24-bit uints, i.e. up to 16 MiB per partition. + n := 3 * (d.nOP - 1) + partLens[d.nOP-1] = d.r.n - n + if partLens[d.nOP-1] < 0 { + return io.ErrUnexpectedEOF + } + if n > 0 { + buf := make([]byte, n) + if err := d.r.ReadFull(buf); err != nil { + return err + } + for i := 0; i < d.nOP-1; i++ { + pl := int(buf[3*i+0]) | int(buf[3*i+1])<<8 | int(buf[3*i+2])<<16 + if pl > partLens[d.nOP-1] { + return io.ErrUnexpectedEOF + } + partLens[i] = pl + partLens[d.nOP-1] -= pl + } + } + + // We check if the final partition length can also fit into a 24-bit uint. + // Strictly speaking, this isn't part of the spec, but it guards against a + // malicious WEBP image that is too large to ReadFull the encoded DCT + // coefficients into memory, whether that's because the actual WEBP file is + // too large, or whether its RIFF metadata lists too large a chunk. + if 1<<24 <= partLens[d.nOP-1] { + return errors.New("vp8: too much data to decode") + } + + buf := make([]byte, d.r.n) + if err := d.r.ReadFull(buf); err != nil { + return err + } + for i, pl := range partLens { + if i == d.nOP { + break + } + d.op[i].init(buf[:pl]) + buf = buf[pl:] + } + return nil +} + +// parseOtherHeaders parses header information other than the frame header. +func (d *Decoder) parseOtherHeaders() error { + // Initialize and parse the first partition. + firstPartition := make([]byte, d.frameHeader.FirstPartitionLen) + if err := d.r.ReadFull(firstPartition); err != nil { + return err + } + d.fp.init(firstPartition) + if d.frameHeader.KeyFrame { + // Read and ignore the color space and pixel clamp values. They are + // specified in section 9.2, but are unimplemented. + d.fp.readBit(uniformProb) + d.fp.readBit(uniformProb) + } + d.parseSegmentHeader() + d.parseFilterHeader() + if err := d.parseOtherPartitions(); err != nil { + return err + } + d.parseQuant() + if !d.frameHeader.KeyFrame { + // Golden and AltRef frames are specified in section 9.7. + // TODO(nigeltao): implement. Note that they are only used for video, not still images. + return errors.New("vp8: Golden / AltRef frames are not implemented") + } + // Read and ignore the refreshLastFrameBuffer bit, specified in section 9.8. + // It applies only to video, and not still images. + d.fp.readBit(uniformProb) + d.parseTokenProb() + d.useSkipProb = d.fp.readBit(uniformProb) + if d.useSkipProb { + d.skipProb = uint8(d.fp.readUint(uniformProb, 8)) + } + if d.fp.unexpectedEOF { + return io.ErrUnexpectedEOF + } + return nil +} + +// DecodeFrame decodes the frame and returns it as an YCbCr image. +// The image's contents are valid up until the next call to Decoder.Init. +func (d *Decoder) DecodeFrame() (*image.YCbCr, error) { + d.ensureImg() + if err := d.parseOtherHeaders(); err != nil { + return nil, err + } + // Reconstruct the rows. + for mbx := 0; mbx < d.mbw; mbx++ { + d.upMB[mbx] = mb{} + } + for mby := 0; mby < d.mbh; mby++ { + d.leftMB = mb{} + for mbx := 0; mbx < d.mbw; mbx++ { + skip := d.reconstruct(mbx, mby) + fs := d.filterParams[d.segment][btou(!d.usePredY16)] + fs.inner = fs.inner || !skip + d.perMBFilterParams[d.mbw*mby+mbx] = fs + } + } + if d.fp.unexpectedEOF { + return nil, io.ErrUnexpectedEOF + } + for i := 0; i < d.nOP; i++ { + if d.op[i].unexpectedEOF { + return nil, io.ErrUnexpectedEOF + } + } + // Apply the loop filter. + // + // Even if we are using per-segment levels, section 15 says that "loop + // filtering must be skipped entirely if loop_filter_level at either the + // frame header level or macroblock override level is 0". + if d.filterHeader.level != 0 { + if d.filterHeader.simple { + d.simpleFilter() + } else { + d.normalFilter() + } + } + return d.img, nil +} diff --git a/vendor/golang.org/x/image/vp8/filter.go b/vendor/golang.org/x/image/vp8/filter.go new file mode 100644 index 00000000..e34a811b --- /dev/null +++ b/vendor/golang.org/x/image/vp8/filter.go @@ -0,0 +1,273 @@ +// Copyright 2014 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. + +package vp8 + +// filter2 modifies a 2-pixel wide or 2-pixel high band along an edge. +func filter2(pix []byte, level, index, iStep, jStep int) { + for n := 16; n > 0; n, index = n-1, index+iStep { + p1 := int(pix[index-2*jStep]) + p0 := int(pix[index-1*jStep]) + q0 := int(pix[index+0*jStep]) + q1 := int(pix[index+1*jStep]) + if abs(p0-q0)<<1+abs(p1-q1)>>1 > level { + continue + } + a := 3*(q0-p0) + clamp127(p1-q1) + a1 := clamp15((a + 4) >> 3) + a2 := clamp15((a + 3) >> 3) + pix[index-1*jStep] = clamp255(p0 + a2) + pix[index+0*jStep] = clamp255(q0 - a1) + } +} + +// filter246 modifies a 2-, 4- or 6-pixel wide or high band along an edge. +func filter246(pix []byte, n, level, ilevel, hlevel, index, iStep, jStep int, fourNotSix bool) { + for ; n > 0; n, index = n-1, index+iStep { + p3 := int(pix[index-4*jStep]) + p2 := int(pix[index-3*jStep]) + p1 := int(pix[index-2*jStep]) + p0 := int(pix[index-1*jStep]) + q0 := int(pix[index+0*jStep]) + q1 := int(pix[index+1*jStep]) + q2 := int(pix[index+2*jStep]) + q3 := int(pix[index+3*jStep]) + if abs(p0-q0)<<1+abs(p1-q1)>>1 > level { + continue + } + if abs(p3-p2) > ilevel || + abs(p2-p1) > ilevel || + abs(p1-p0) > ilevel || + abs(q1-q0) > ilevel || + abs(q2-q1) > ilevel || + abs(q3-q2) > ilevel { + continue + } + if abs(p1-p0) > hlevel || abs(q1-q0) > hlevel { + // Filter 2 pixels. + a := 3*(q0-p0) + clamp127(p1-q1) + a1 := clamp15((a + 4) >> 3) + a2 := clamp15((a + 3) >> 3) + pix[index-1*jStep] = clamp255(p0 + a2) + pix[index+0*jStep] = clamp255(q0 - a1) + } else if fourNotSix { + // Filter 4 pixels. + a := 3 * (q0 - p0) + a1 := clamp15((a + 4) >> 3) + a2 := clamp15((a + 3) >> 3) + a3 := (a1 + 1) >> 1 + pix[index-2*jStep] = clamp255(p1 + a3) + pix[index-1*jStep] = clamp255(p0 + a2) + pix[index+0*jStep] = clamp255(q0 - a1) + pix[index+1*jStep] = clamp255(q1 - a3) + } else { + // Filter 6 pixels. + a := clamp127(3*(q0-p0) + clamp127(p1-q1)) + a1 := (27*a + 63) >> 7 + a2 := (18*a + 63) >> 7 + a3 := (9*a + 63) >> 7 + pix[index-3*jStep] = clamp255(p2 + a3) + pix[index-2*jStep] = clamp255(p1 + a2) + pix[index-1*jStep] = clamp255(p0 + a1) + pix[index+0*jStep] = clamp255(q0 - a1) + pix[index+1*jStep] = clamp255(q1 - a2) + pix[index+2*jStep] = clamp255(q2 - a3) + } + } +} + +// simpleFilter implements the simple filter, as specified in section 15.2. +func (d *Decoder) simpleFilter() { + for mby := 0; mby < d.mbh; mby++ { + for mbx := 0; mbx < d.mbw; mbx++ { + f := d.perMBFilterParams[d.mbw*mby+mbx] + if f.level == 0 { + continue + } + l := int(f.level) + yIndex := (mby*d.img.YStride + mbx) * 16 + if mbx > 0 { + filter2(d.img.Y, l+4, yIndex, d.img.YStride, 1) + } + if f.inner { + filter2(d.img.Y, l, yIndex+0x4, d.img.YStride, 1) + filter2(d.img.Y, l, yIndex+0x8, d.img.YStride, 1) + filter2(d.img.Y, l, yIndex+0xc, d.img.YStride, 1) + } + if mby > 0 { + filter2(d.img.Y, l+4, yIndex, 1, d.img.YStride) + } + if f.inner { + filter2(d.img.Y, l, yIndex+d.img.YStride*0x4, 1, d.img.YStride) + filter2(d.img.Y, l, yIndex+d.img.YStride*0x8, 1, d.img.YStride) + filter2(d.img.Y, l, yIndex+d.img.YStride*0xc, 1, d.img.YStride) + } + } + } +} + +// normalFilter implements the normal filter, as specified in section 15.3. +func (d *Decoder) normalFilter() { + for mby := 0; mby < d.mbh; mby++ { + for mbx := 0; mbx < d.mbw; mbx++ { + f := d.perMBFilterParams[d.mbw*mby+mbx] + if f.level == 0 { + continue + } + l, il, hl := int(f.level), int(f.ilevel), int(f.hlevel) + yIndex := (mby*d.img.YStride + mbx) * 16 + cIndex := (mby*d.img.CStride + mbx) * 8 + if mbx > 0 { + filter246(d.img.Y, 16, l+4, il, hl, yIndex, d.img.YStride, 1, false) + filter246(d.img.Cb, 8, l+4, il, hl, cIndex, d.img.CStride, 1, false) + filter246(d.img.Cr, 8, l+4, il, hl, cIndex, d.img.CStride, 1, false) + } + if f.inner { + filter246(d.img.Y, 16, l, il, hl, yIndex+0x4, d.img.YStride, 1, true) + filter246(d.img.Y, 16, l, il, hl, yIndex+0x8, d.img.YStride, 1, true) + filter246(d.img.Y, 16, l, il, hl, yIndex+0xc, d.img.YStride, 1, true) + filter246(d.img.Cb, 8, l, il, hl, cIndex+0x4, d.img.CStride, 1, true) + filter246(d.img.Cr, 8, l, il, hl, cIndex+0x4, d.img.CStride, 1, true) + } + if mby > 0 { + filter246(d.img.Y, 16, l+4, il, hl, yIndex, 1, d.img.YStride, false) + filter246(d.img.Cb, 8, l+4, il, hl, cIndex, 1, d.img.CStride, false) + filter246(d.img.Cr, 8, l+4, il, hl, cIndex, 1, d.img.CStride, false) + } + if f.inner { + filter246(d.img.Y, 16, l, il, hl, yIndex+d.img.YStride*0x4, 1, d.img.YStride, true) + filter246(d.img.Y, 16, l, il, hl, yIndex+d.img.YStride*0x8, 1, d.img.YStride, true) + filter246(d.img.Y, 16, l, il, hl, yIndex+d.img.YStride*0xc, 1, d.img.YStride, true) + filter246(d.img.Cb, 8, l, il, hl, cIndex+d.img.CStride*0x4, 1, d.img.CStride, true) + filter246(d.img.Cr, 8, l, il, hl, cIndex+d.img.CStride*0x4, 1, d.img.CStride, true) + } + } + } +} + +// filterParam holds the loop filter parameters for a macroblock. +type filterParam struct { + // The first three fields are thresholds used by the loop filter to smooth + // over the edges and interior of a macroblock. level is used by both the + // simple and normal filters. The inner level and high edge variance level + // are only used by the normal filter. + level, ilevel, hlevel uint8 + // inner is whether the inner loop filter cannot be optimized out as a + // no-op for this particular macroblock. + inner bool +} + +// computeFilterParams computes the loop filter parameters, as specified in +// section 15.4. +func (d *Decoder) computeFilterParams() { + for i := range d.filterParams { + baseLevel := d.filterHeader.level + if d.segmentHeader.useSegment { + baseLevel = d.segmentHeader.filterStrength[i] + if d.segmentHeader.relativeDelta { + baseLevel += d.filterHeader.level + } + } + + for j := range d.filterParams[i] { + p := &d.filterParams[i][j] + p.inner = j != 0 + level := baseLevel + if d.filterHeader.useLFDelta { + // The libwebp C code has a "TODO: only CURRENT is handled for now." + level += d.filterHeader.refLFDelta[0] + if j != 0 { + level += d.filterHeader.modeLFDelta[0] + } + } + if level <= 0 { + p.level = 0 + continue + } + if level > 63 { + level = 63 + } + ilevel := level + if d.filterHeader.sharpness > 0 { + if d.filterHeader.sharpness > 4 { + ilevel >>= 2 + } else { + ilevel >>= 1 + } + if x := int8(9 - d.filterHeader.sharpness); ilevel > x { + ilevel = x + } + } + if ilevel < 1 { + ilevel = 1 + } + p.ilevel = uint8(ilevel) + p.level = uint8(2*level + ilevel) + if d.frameHeader.KeyFrame { + if level < 15 { + p.hlevel = 0 + } else if level < 40 { + p.hlevel = 1 + } else { + p.hlevel = 2 + } + } else { + if level < 15 { + p.hlevel = 0 + } else if level < 20 { + p.hlevel = 1 + } else if level < 40 { + p.hlevel = 2 + } else { + p.hlevel = 3 + } + } + } + } +} + +// intSize is either 32 or 64. +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 +} + +func clamp15(x int) int { + if x < -16 { + return -16 + } + if x > 15 { + return 15 + } + return x +} + +func clamp127(x int) int { + if x < -128 { + return -128 + } + if x > 127 { + return 127 + } + return x +} + +func clamp255(x int) uint8 { + if x < 0 { + return 0 + } + if x > 255 { + return 255 + } + return uint8(x) +} diff --git a/vendor/golang.org/x/image/vp8/idct.go b/vendor/golang.org/x/image/vp8/idct.go new file mode 100644 index 00000000..929af2cc --- /dev/null +++ b/vendor/golang.org/x/image/vp8/idct.go @@ -0,0 +1,98 @@ +// Copyright 2011 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. + +package vp8 + +// This file implements the inverse Discrete Cosine Transform and the inverse +// Walsh Hadamard Transform (WHT), as specified in sections 14.3 and 14.4. + +func clip8(i int32) uint8 { + if i < 0 { + return 0 + } + if i > 255 { + return 255 + } + return uint8(i) +} + +func (z *Decoder) inverseDCT4(y, x, coeffBase int) { + const ( + c1 = 85627 // 65536 * cos(pi/8) * sqrt(2). + c2 = 35468 // 65536 * sin(pi/8) * sqrt(2). + ) + var m [4][4]int32 + for i := 0; i < 4; i++ { + a := int32(z.coeff[coeffBase+0]) + int32(z.coeff[coeffBase+8]) + b := int32(z.coeff[coeffBase+0]) - int32(z.coeff[coeffBase+8]) + c := (int32(z.coeff[coeffBase+4])*c2)>>16 - (int32(z.coeff[coeffBase+12])*c1)>>16 + d := (int32(z.coeff[coeffBase+4])*c1)>>16 + (int32(z.coeff[coeffBase+12])*c2)>>16 + m[i][0] = a + d + m[i][1] = b + c + m[i][2] = b - c + m[i][3] = a - d + coeffBase++ + } + for j := 0; j < 4; j++ { + dc := m[0][j] + 4 + a := dc + m[2][j] + b := dc - m[2][j] + c := (m[1][j]*c2)>>16 - (m[3][j]*c1)>>16 + d := (m[1][j]*c1)>>16 + (m[3][j]*c2)>>16 + z.ybr[y+j][x+0] = clip8(int32(z.ybr[y+j][x+0]) + (a+d)>>3) + z.ybr[y+j][x+1] = clip8(int32(z.ybr[y+j][x+1]) + (b+c)>>3) + z.ybr[y+j][x+2] = clip8(int32(z.ybr[y+j][x+2]) + (b-c)>>3) + z.ybr[y+j][x+3] = clip8(int32(z.ybr[y+j][x+3]) + (a-d)>>3) + } +} + +func (z *Decoder) inverseDCT4DCOnly(y, x, coeffBase int) { + dc := (int32(z.coeff[coeffBase+0]) + 4) >> 3 + for j := 0; j < 4; j++ { + for i := 0; i < 4; i++ { + z.ybr[y+j][x+i] = clip8(int32(z.ybr[y+j][x+i]) + dc) + } + } +} + +func (z *Decoder) inverseDCT8(y, x, coeffBase int) { + z.inverseDCT4(y+0, x+0, coeffBase+0*16) + z.inverseDCT4(y+0, x+4, coeffBase+1*16) + z.inverseDCT4(y+4, x+0, coeffBase+2*16) + z.inverseDCT4(y+4, x+4, coeffBase+3*16) +} + +func (z *Decoder) inverseDCT8DCOnly(y, x, coeffBase int) { + z.inverseDCT4DCOnly(y+0, x+0, coeffBase+0*16) + z.inverseDCT4DCOnly(y+0, x+4, coeffBase+1*16) + z.inverseDCT4DCOnly(y+4, x+0, coeffBase+2*16) + z.inverseDCT4DCOnly(y+4, x+4, coeffBase+3*16) +} + +func (d *Decoder) inverseWHT16() { + var m [16]int32 + for i := 0; i < 4; i++ { + a0 := int32(d.coeff[384+0+i]) + int32(d.coeff[384+12+i]) + a1 := int32(d.coeff[384+4+i]) + int32(d.coeff[384+8+i]) + a2 := int32(d.coeff[384+4+i]) - int32(d.coeff[384+8+i]) + a3 := int32(d.coeff[384+0+i]) - int32(d.coeff[384+12+i]) + m[0+i] = a0 + a1 + m[8+i] = a0 - a1 + m[4+i] = a3 + a2 + m[12+i] = a3 - a2 + } + out := 0 + for i := 0; i < 4; i++ { + dc := m[0+i*4] + 3 + a0 := dc + m[3+i*4] + a1 := m[1+i*4] + m[2+i*4] + a2 := m[1+i*4] - m[2+i*4] + a3 := dc - m[3+i*4] + d.coeff[out+0] = int16((a0 + a1) >> 3) + d.coeff[out+16] = int16((a3 + a2) >> 3) + d.coeff[out+32] = int16((a0 - a1) >> 3) + d.coeff[out+48] = int16((a3 - a2) >> 3) + out += 64 + } +} diff --git a/vendor/golang.org/x/image/vp8/partition.go b/vendor/golang.org/x/image/vp8/partition.go new file mode 100644 index 00000000..72288bde --- /dev/null +++ b/vendor/golang.org/x/image/vp8/partition.go @@ -0,0 +1,129 @@ +// Copyright 2011 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. + +package vp8 + +// Each VP8 frame consists of between 2 and 9 bitstream partitions. +// Each partition is byte-aligned and is independently arithmetic-encoded. +// +// This file implements decoding a partition's bitstream, as specified in +// chapter 7. The implementation follows libwebp's approach instead of the +// specification's reference C implementation. For example, we use a look-up +// table instead of a for loop to recalibrate the encoded range. + +var ( + lutShift = [127]uint8{ + 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + } + lutRangeM1 = [127]uint8{ + 127, + 127, 191, + 127, 159, 191, 223, + 127, 143, 159, 175, 191, 207, 223, 239, + 127, 135, 143, 151, 159, 167, 175, 183, 191, 199, 207, 215, 223, 231, 239, 247, + 127, 131, 135, 139, 143, 147, 151, 155, 159, 163, 167, 171, 175, 179, 183, 187, + 191, 195, 199, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, + 127, 129, 131, 133, 135, 137, 139, 141, 143, 145, 147, 149, 151, 153, 155, 157, + 159, 161, 163, 165, 167, 169, 171, 173, 175, 177, 179, 181, 183, 185, 187, 189, + 191, 193, 195, 197, 199, 201, 203, 205, 207, 209, 211, 213, 215, 217, 219, 221, + 223, 225, 227, 229, 231, 233, 235, 237, 239, 241, 243, 245, 247, 249, 251, 253, + } +) + +// uniformProb represents a 50% probability that the next bit is 0. +const uniformProb = 128 + +// partition holds arithmetic-coded bits. +type partition struct { + // buf is the input bytes. + buf []byte + // r is how many of buf's bytes have been consumed. + r int + // rangeM1 is range minus 1, where range is in the arithmetic coding sense, + // not the Go language sense. + rangeM1 uint32 + // bits and nBits hold those bits shifted out of buf but not yet consumed. + bits uint32 + nBits uint8 + // unexpectedEOF tells whether we tried to read past buf. + unexpectedEOF bool +} + +// init initializes the partition. +func (p *partition) init(buf []byte) { + p.buf = buf + p.r = 0 + p.rangeM1 = 254 + p.bits = 0 + p.nBits = 0 + p.unexpectedEOF = false +} + +// readBit returns the next bit. +func (p *partition) readBit(prob uint8) bool { + if p.nBits < 8 { + if p.r >= len(p.buf) { + p.unexpectedEOF = true + return false + } + // Expression split for 386 compiler. + x := uint32(p.buf[p.r]) + p.bits |= x << (8 - p.nBits) + p.r++ + p.nBits += 8 + } + split := (p.rangeM1*uint32(prob))>>8 + 1 + bit := p.bits >= split<<8 + if bit { + p.rangeM1 -= split + p.bits -= split << 8 + } else { + p.rangeM1 = split - 1 + } + if p.rangeM1 < 127 { + shift := lutShift[p.rangeM1] + p.rangeM1 = uint32(lutRangeM1[p.rangeM1]) + p.bits <<= shift + p.nBits -= shift + } + return bit +} + +// readUint returns the next n-bit unsigned integer. +func (p *partition) readUint(prob, n uint8) uint32 { + var u uint32 + for n > 0 { + n-- + if p.readBit(prob) { + u |= 1 << n + } + } + return u +} + +// readInt returns the next n-bit signed integer. +func (p *partition) readInt(prob, n uint8) int32 { + u := p.readUint(prob, n) + b := p.readBit(prob) + if b { + return -int32(u) + } + return int32(u) +} + +// readOptionalInt returns the next n-bit signed integer in an encoding +// where the likely result is zero. +func (p *partition) readOptionalInt(prob, n uint8) int32 { + if !p.readBit(prob) { + return 0 + } + return p.readInt(prob, n) +} diff --git a/vendor/golang.org/x/image/vp8/pred.go b/vendor/golang.org/x/image/vp8/pred.go new file mode 100644 index 00000000..58c2689e --- /dev/null +++ b/vendor/golang.org/x/image/vp8/pred.go @@ -0,0 +1,201 @@ +// Copyright 2011 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. + +package vp8 + +// This file implements parsing the predictor modes, as specified in chapter +// 11. + +func (d *Decoder) parsePredModeY16(mbx int) { + var p uint8 + if !d.fp.readBit(156) { + if !d.fp.readBit(163) { + p = predDC + } else { + p = predVE + } + } else if !d.fp.readBit(128) { + p = predHE + } else { + p = predTM + } + for i := 0; i < 4; i++ { + d.upMB[mbx].pred[i] = p + d.leftMB.pred[i] = p + } + d.predY16 = p +} + +func (d *Decoder) parsePredModeC8() { + if !d.fp.readBit(142) { + d.predC8 = predDC + } else if !d.fp.readBit(114) { + d.predC8 = predVE + } else if !d.fp.readBit(183) { + d.predC8 = predHE + } else { + d.predC8 = predTM + } +} + +func (d *Decoder) parsePredModeY4(mbx int) { + for j := 0; j < 4; j++ { + p := d.leftMB.pred[j] + for i := 0; i < 4; i++ { + prob := &predProb[d.upMB[mbx].pred[i]][p] + if !d.fp.readBit(prob[0]) { + p = predDC + } else if !d.fp.readBit(prob[1]) { + p = predTM + } else if !d.fp.readBit(prob[2]) { + p = predVE + } else if !d.fp.readBit(prob[3]) { + if !d.fp.readBit(prob[4]) { + p = predHE + } else if !d.fp.readBit(prob[5]) { + p = predRD + } else { + p = predVR + } + } else if !d.fp.readBit(prob[6]) { + p = predLD + } else if !d.fp.readBit(prob[7]) { + p = predVL + } else if !d.fp.readBit(prob[8]) { + p = predHD + } else { + p = predHU + } + d.predY4[j][i] = p + d.upMB[mbx].pred[i] = p + } + d.leftMB.pred[j] = p + } +} + +// predProb are the probabilities to decode a 4x4 region's predictor mode given +// the predictor modes of the regions above and left of it. +// These values are specified in section 11.5. +var predProb = [nPred][nPred][9]uint8{ + { + {231, 120, 48, 89, 115, 113, 120, 152, 112}, + {152, 179, 64, 126, 170, 118, 46, 70, 95}, + {175, 69, 143, 80, 85, 82, 72, 155, 103}, + {56, 58, 10, 171, 218, 189, 17, 13, 152}, + {114, 26, 17, 163, 44, 195, 21, 10, 173}, + {121, 24, 80, 195, 26, 62, 44, 64, 85}, + {144, 71, 10, 38, 171, 213, 144, 34, 26}, + {170, 46, 55, 19, 136, 160, 33, 206, 71}, + {63, 20, 8, 114, 114, 208, 12, 9, 226}, + {81, 40, 11, 96, 182, 84, 29, 16, 36}, + }, + { + {134, 183, 89, 137, 98, 101, 106, 165, 148}, + {72, 187, 100, 130, 157, 111, 32, 75, 80}, + {66, 102, 167, 99, 74, 62, 40, 234, 128}, + {41, 53, 9, 178, 241, 141, 26, 8, 107}, + {74, 43, 26, 146, 73, 166, 49, 23, 157}, + {65, 38, 105, 160, 51, 52, 31, 115, 128}, + {104, 79, 12, 27, 217, 255, 87, 17, 7}, + {87, 68, 71, 44, 114, 51, 15, 186, 23}, + {47, 41, 14, 110, 182, 183, 21, 17, 194}, + {66, 45, 25, 102, 197, 189, 23, 18, 22}, + }, + { + {88, 88, 147, 150, 42, 46, 45, 196, 205}, + {43, 97, 183, 117, 85, 38, 35, 179, 61}, + {39, 53, 200, 87, 26, 21, 43, 232, 171}, + {56, 34, 51, 104, 114, 102, 29, 93, 77}, + {39, 28, 85, 171, 58, 165, 90, 98, 64}, + {34, 22, 116, 206, 23, 34, 43, 166, 73}, + {107, 54, 32, 26, 51, 1, 81, 43, 31}, + {68, 25, 106, 22, 64, 171, 36, 225, 114}, + {34, 19, 21, 102, 132, 188, 16, 76, 124}, + {62, 18, 78, 95, 85, 57, 50, 48, 51}, + }, + { + {193, 101, 35, 159, 215, 111, 89, 46, 111}, + {60, 148, 31, 172, 219, 228, 21, 18, 111}, + {112, 113, 77, 85, 179, 255, 38, 120, 114}, + {40, 42, 1, 196, 245, 209, 10, 25, 109}, + {88, 43, 29, 140, 166, 213, 37, 43, 154}, + {61, 63, 30, 155, 67, 45, 68, 1, 209}, + {100, 80, 8, 43, 154, 1, 51, 26, 71}, + {142, 78, 78, 16, 255, 128, 34, 197, 171}, + {41, 40, 5, 102, 211, 183, 4, 1, 221}, + {51, 50, 17, 168, 209, 192, 23, 25, 82}, + }, + { + {138, 31, 36, 171, 27, 166, 38, 44, 229}, + {67, 87, 58, 169, 82, 115, 26, 59, 179}, + {63, 59, 90, 180, 59, 166, 93, 73, 154}, + {40, 40, 21, 116, 143, 209, 34, 39, 175}, + {47, 15, 16, 183, 34, 223, 49, 45, 183}, + {46, 17, 33, 183, 6, 98, 15, 32, 183}, + {57, 46, 22, 24, 128, 1, 54, 17, 37}, + {65, 32, 73, 115, 28, 128, 23, 128, 205}, + {40, 3, 9, 115, 51, 192, 18, 6, 223}, + {87, 37, 9, 115, 59, 77, 64, 21, 47}, + }, + { + {104, 55, 44, 218, 9, 54, 53, 130, 226}, + {64, 90, 70, 205, 40, 41, 23, 26, 57}, + {54, 57, 112, 184, 5, 41, 38, 166, 213}, + {30, 34, 26, 133, 152, 116, 10, 32, 134}, + {39, 19, 53, 221, 26, 114, 32, 73, 255}, + {31, 9, 65, 234, 2, 15, 1, 118, 73}, + {75, 32, 12, 51, 192, 255, 160, 43, 51}, + {88, 31, 35, 67, 102, 85, 55, 186, 85}, + {56, 21, 23, 111, 59, 205, 45, 37, 192}, + {55, 38, 70, 124, 73, 102, 1, 34, 98}, + }, + { + {125, 98, 42, 88, 104, 85, 117, 175, 82}, + {95, 84, 53, 89, 128, 100, 113, 101, 45}, + {75, 79, 123, 47, 51, 128, 81, 171, 1}, + {57, 17, 5, 71, 102, 57, 53, 41, 49}, + {38, 33, 13, 121, 57, 73, 26, 1, 85}, + {41, 10, 67, 138, 77, 110, 90, 47, 114}, + {115, 21, 2, 10, 102, 255, 166, 23, 6}, + {101, 29, 16, 10, 85, 128, 101, 196, 26}, + {57, 18, 10, 102, 102, 213, 34, 20, 43}, + {117, 20, 15, 36, 163, 128, 68, 1, 26}, + }, + { + {102, 61, 71, 37, 34, 53, 31, 243, 192}, + {69, 60, 71, 38, 73, 119, 28, 222, 37}, + {68, 45, 128, 34, 1, 47, 11, 245, 171}, + {62, 17, 19, 70, 146, 85, 55, 62, 70}, + {37, 43, 37, 154, 100, 163, 85, 160, 1}, + {63, 9, 92, 136, 28, 64, 32, 201, 85}, + {75, 15, 9, 9, 64, 255, 184, 119, 16}, + {86, 6, 28, 5, 64, 255, 25, 248, 1}, + {56, 8, 17, 132, 137, 255, 55, 116, 128}, + {58, 15, 20, 82, 135, 57, 26, 121, 40}, + }, + { + {164, 50, 31, 137, 154, 133, 25, 35, 218}, + {51, 103, 44, 131, 131, 123, 31, 6, 158}, + {86, 40, 64, 135, 148, 224, 45, 183, 128}, + {22, 26, 17, 131, 240, 154, 14, 1, 209}, + {45, 16, 21, 91, 64, 222, 7, 1, 197}, + {56, 21, 39, 155, 60, 138, 23, 102, 213}, + {83, 12, 13, 54, 192, 255, 68, 47, 28}, + {85, 26, 85, 85, 128, 128, 32, 146, 171}, + {18, 11, 7, 63, 144, 171, 4, 4, 246}, + {35, 27, 10, 146, 174, 171, 12, 26, 128}, + }, + { + {190, 80, 35, 99, 180, 80, 126, 54, 45}, + {85, 126, 47, 87, 176, 51, 41, 20, 32}, + {101, 75, 128, 139, 118, 146, 116, 128, 85}, + {56, 41, 15, 176, 236, 85, 37, 9, 62}, + {71, 30, 17, 119, 118, 255, 17, 18, 138}, + {101, 38, 60, 138, 55, 70, 43, 26, 142}, + {146, 36, 19, 30, 171, 255, 97, 27, 20}, + {138, 45, 61, 62, 219, 1, 81, 188, 64}, + {32, 41, 20, 117, 151, 142, 20, 21, 163}, + {112, 19, 12, 61, 195, 128, 48, 4, 24}, + }, +} diff --git a/vendor/golang.org/x/image/vp8/predfunc.go b/vendor/golang.org/x/image/vp8/predfunc.go new file mode 100644 index 00000000..f8999582 --- /dev/null +++ b/vendor/golang.org/x/image/vp8/predfunc.go @@ -0,0 +1,553 @@ +// Copyright 2011 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. + +package vp8 + +// This file implements the predicition functions, as specified in chapter 12. +// +// For each macroblock (of 1x16x16 luma and 2x8x8 chroma coefficients), the +// luma values are either predicted as one large 16x16 region or 16 separate +// 4x4 regions. The chroma values are always predicted as one 8x8 region. +// +// For 4x4 regions, the target block's predicted values (Xs) are a function of +// its previously-decoded top and left border values, as well as a number of +// pixels from the top-right: +// +// a b c d e f g h +// p X X X X +// q X X X X +// r X X X X +// s X X X X +// +// The predictor modes are: +// - DC: all Xs = (b + c + d + e + p + q + r + s + 4) / 8. +// - TM: the first X = (b + p - a), the second X = (c + p - a), and so on. +// - VE: each X = the weighted average of its column's top value and that +// value's neighbors, i.e. averages of abc, bcd, cde or def. +// - HE: similar to VE except rows instead of columns, and the final row is +// an average of r, s and s. +// - RD, VR, LD, VL, HD, HU: these diagonal modes ("Right Down", "Vertical +// Right", etc) are more complicated and are described in section 12.3. +// All Xs are clipped to the range [0, 255]. +// +// For 8x8 and 16x16 regions, the target block's predicted values are a +// function of the top and left border values without the top-right overhang, +// i.e. without the 8x8 or 16x16 equivalent of f, g and h. Furthermore: +// - There are no diagonal predictor modes, only DC, TM, VE and HE. +// - The DC mode has variants for macroblocks in the top row and/or left +// column, i.e. for macroblocks with mby == 0 || mbx == 0. +// - The VE and HE modes take only the column top or row left values; they do +// not smooth that top/left value with its neighbors. + +// nPred is the number of predictor modes, not including the Top/Left versions +// of the DC predictor mode. +const nPred = 10 + +const ( + predDC = iota + predTM + predVE + predHE + predRD + predVR + predLD + predVL + predHD + predHU + predDCTop + predDCLeft + predDCTopLeft +) + +func checkTopLeftPred(mbx, mby int, p uint8) uint8 { + if p != predDC { + return p + } + if mbx == 0 { + if mby == 0 { + return predDCTopLeft + } + return predDCLeft + } + if mby == 0 { + return predDCTop + } + return predDC +} + +var predFunc4 = [...]func(*Decoder, int, int){ + predFunc4DC, + predFunc4TM, + predFunc4VE, + predFunc4HE, + predFunc4RD, + predFunc4VR, + predFunc4LD, + predFunc4VL, + predFunc4HD, + predFunc4HU, + nil, + nil, + nil, +} + +var predFunc8 = [...]func(*Decoder, int, int){ + predFunc8DC, + predFunc8TM, + predFunc8VE, + predFunc8HE, + nil, + nil, + nil, + nil, + nil, + nil, + predFunc8DCTop, + predFunc8DCLeft, + predFunc8DCTopLeft, +} + +var predFunc16 = [...]func(*Decoder, int, int){ + predFunc16DC, + predFunc16TM, + predFunc16VE, + predFunc16HE, + nil, + nil, + nil, + nil, + nil, + nil, + predFunc16DCTop, + predFunc16DCLeft, + predFunc16DCTopLeft, +} + +func predFunc4DC(z *Decoder, y, x int) { + sum := uint32(4) + for i := 0; i < 4; i++ { + sum += uint32(z.ybr[y-1][x+i]) + } + for j := 0; j < 4; j++ { + sum += uint32(z.ybr[y+j][x-1]) + } + avg := uint8(sum / 8) + for j := 0; j < 4; j++ { + for i := 0; i < 4; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc4TM(z *Decoder, y, x int) { + delta0 := -int32(z.ybr[y-1][x-1]) + for j := 0; j < 4; j++ { + delta1 := delta0 + int32(z.ybr[y+j][x-1]) + for i := 0; i < 4; i++ { + delta2 := delta1 + int32(z.ybr[y-1][x+i]) + z.ybr[y+j][x+i] = uint8(clip(delta2, 0, 255)) + } + } +} + +func predFunc4VE(z *Decoder, y, x int) { + a := int32(z.ybr[y-1][x-1]) + b := int32(z.ybr[y-1][x+0]) + c := int32(z.ybr[y-1][x+1]) + d := int32(z.ybr[y-1][x+2]) + e := int32(z.ybr[y-1][x+3]) + f := int32(z.ybr[y-1][x+4]) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + cde := uint8((c + 2*d + e + 2) / 4) + def := uint8((d + 2*e + f + 2) / 4) + for j := 0; j < 4; j++ { + z.ybr[y+j][x+0] = abc + z.ybr[y+j][x+1] = bcd + z.ybr[y+j][x+2] = cde + z.ybr[y+j][x+3] = def + } +} + +func predFunc4HE(z *Decoder, y, x int) { + s := int32(z.ybr[y+3][x-1]) + r := int32(z.ybr[y+2][x-1]) + q := int32(z.ybr[y+1][x-1]) + p := int32(z.ybr[y+0][x-1]) + a := int32(z.ybr[y-1][x-1]) + ssr := uint8((s + 2*s + r + 2) / 4) + srq := uint8((s + 2*r + q + 2) / 4) + rqp := uint8((r + 2*q + p + 2) / 4) + apq := uint8((a + 2*p + q + 2) / 4) + for i := 0; i < 4; i++ { + z.ybr[y+0][x+i] = apq + z.ybr[y+1][x+i] = rqp + z.ybr[y+2][x+i] = srq + z.ybr[y+3][x+i] = ssr + } +} + +func predFunc4RD(z *Decoder, y, x int) { + s := int32(z.ybr[y+3][x-1]) + r := int32(z.ybr[y+2][x-1]) + q := int32(z.ybr[y+1][x-1]) + p := int32(z.ybr[y+0][x-1]) + a := int32(z.ybr[y-1][x-1]) + b := int32(z.ybr[y-1][x+0]) + c := int32(z.ybr[y-1][x+1]) + d := int32(z.ybr[y-1][x+2]) + e := int32(z.ybr[y-1][x+3]) + srq := uint8((s + 2*r + q + 2) / 4) + rqp := uint8((r + 2*q + p + 2) / 4) + qpa := uint8((q + 2*p + a + 2) / 4) + pab := uint8((p + 2*a + b + 2) / 4) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + cde := uint8((c + 2*d + e + 2) / 4) + z.ybr[y+0][x+0] = pab + z.ybr[y+0][x+1] = abc + z.ybr[y+0][x+2] = bcd + z.ybr[y+0][x+3] = cde + z.ybr[y+1][x+0] = qpa + z.ybr[y+1][x+1] = pab + z.ybr[y+1][x+2] = abc + z.ybr[y+1][x+3] = bcd + z.ybr[y+2][x+0] = rqp + z.ybr[y+2][x+1] = qpa + z.ybr[y+2][x+2] = pab + z.ybr[y+2][x+3] = abc + z.ybr[y+3][x+0] = srq + z.ybr[y+3][x+1] = rqp + z.ybr[y+3][x+2] = qpa + z.ybr[y+3][x+3] = pab +} + +func predFunc4VR(z *Decoder, y, x int) { + r := int32(z.ybr[y+2][x-1]) + q := int32(z.ybr[y+1][x-1]) + p := int32(z.ybr[y+0][x-1]) + a := int32(z.ybr[y-1][x-1]) + b := int32(z.ybr[y-1][x+0]) + c := int32(z.ybr[y-1][x+1]) + d := int32(z.ybr[y-1][x+2]) + e := int32(z.ybr[y-1][x+3]) + ab := uint8((a + b + 1) / 2) + bc := uint8((b + c + 1) / 2) + cd := uint8((c + d + 1) / 2) + de := uint8((d + e + 1) / 2) + rqp := uint8((r + 2*q + p + 2) / 4) + qpa := uint8((q + 2*p + a + 2) / 4) + pab := uint8((p + 2*a + b + 2) / 4) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + cde := uint8((c + 2*d + e + 2) / 4) + z.ybr[y+0][x+0] = ab + z.ybr[y+0][x+1] = bc + z.ybr[y+0][x+2] = cd + z.ybr[y+0][x+3] = de + z.ybr[y+1][x+0] = pab + z.ybr[y+1][x+1] = abc + z.ybr[y+1][x+2] = bcd + z.ybr[y+1][x+3] = cde + z.ybr[y+2][x+0] = qpa + z.ybr[y+2][x+1] = ab + z.ybr[y+2][x+2] = bc + z.ybr[y+2][x+3] = cd + z.ybr[y+3][x+0] = rqp + z.ybr[y+3][x+1] = pab + z.ybr[y+3][x+2] = abc + z.ybr[y+3][x+3] = bcd +} + +func predFunc4LD(z *Decoder, y, x int) { + a := int32(z.ybr[y-1][x+0]) + b := int32(z.ybr[y-1][x+1]) + c := int32(z.ybr[y-1][x+2]) + d := int32(z.ybr[y-1][x+3]) + e := int32(z.ybr[y-1][x+4]) + f := int32(z.ybr[y-1][x+5]) + g := int32(z.ybr[y-1][x+6]) + h := int32(z.ybr[y-1][x+7]) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + cde := uint8((c + 2*d + e + 2) / 4) + def := uint8((d + 2*e + f + 2) / 4) + efg := uint8((e + 2*f + g + 2) / 4) + fgh := uint8((f + 2*g + h + 2) / 4) + ghh := uint8((g + 2*h + h + 2) / 4) + z.ybr[y+0][x+0] = abc + z.ybr[y+0][x+1] = bcd + z.ybr[y+0][x+2] = cde + z.ybr[y+0][x+3] = def + z.ybr[y+1][x+0] = bcd + z.ybr[y+1][x+1] = cde + z.ybr[y+1][x+2] = def + z.ybr[y+1][x+3] = efg + z.ybr[y+2][x+0] = cde + z.ybr[y+2][x+1] = def + z.ybr[y+2][x+2] = efg + z.ybr[y+2][x+3] = fgh + z.ybr[y+3][x+0] = def + z.ybr[y+3][x+1] = efg + z.ybr[y+3][x+2] = fgh + z.ybr[y+3][x+3] = ghh +} + +func predFunc4VL(z *Decoder, y, x int) { + a := int32(z.ybr[y-1][x+0]) + b := int32(z.ybr[y-1][x+1]) + c := int32(z.ybr[y-1][x+2]) + d := int32(z.ybr[y-1][x+3]) + e := int32(z.ybr[y-1][x+4]) + f := int32(z.ybr[y-1][x+5]) + g := int32(z.ybr[y-1][x+6]) + h := int32(z.ybr[y-1][x+7]) + ab := uint8((a + b + 1) / 2) + bc := uint8((b + c + 1) / 2) + cd := uint8((c + d + 1) / 2) + de := uint8((d + e + 1) / 2) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + cde := uint8((c + 2*d + e + 2) / 4) + def := uint8((d + 2*e + f + 2) / 4) + efg := uint8((e + 2*f + g + 2) / 4) + fgh := uint8((f + 2*g + h + 2) / 4) + z.ybr[y+0][x+0] = ab + z.ybr[y+0][x+1] = bc + z.ybr[y+0][x+2] = cd + z.ybr[y+0][x+3] = de + z.ybr[y+1][x+0] = abc + z.ybr[y+1][x+1] = bcd + z.ybr[y+1][x+2] = cde + z.ybr[y+1][x+3] = def + z.ybr[y+2][x+0] = bc + z.ybr[y+2][x+1] = cd + z.ybr[y+2][x+2] = de + z.ybr[y+2][x+3] = efg + z.ybr[y+3][x+0] = bcd + z.ybr[y+3][x+1] = cde + z.ybr[y+3][x+2] = def + z.ybr[y+3][x+3] = fgh +} + +func predFunc4HD(z *Decoder, y, x int) { + s := int32(z.ybr[y+3][x-1]) + r := int32(z.ybr[y+2][x-1]) + q := int32(z.ybr[y+1][x-1]) + p := int32(z.ybr[y+0][x-1]) + a := int32(z.ybr[y-1][x-1]) + b := int32(z.ybr[y-1][x+0]) + c := int32(z.ybr[y-1][x+1]) + d := int32(z.ybr[y-1][x+2]) + sr := uint8((s + r + 1) / 2) + rq := uint8((r + q + 1) / 2) + qp := uint8((q + p + 1) / 2) + pa := uint8((p + a + 1) / 2) + srq := uint8((s + 2*r + q + 2) / 4) + rqp := uint8((r + 2*q + p + 2) / 4) + qpa := uint8((q + 2*p + a + 2) / 4) + pab := uint8((p + 2*a + b + 2) / 4) + abc := uint8((a + 2*b + c + 2) / 4) + bcd := uint8((b + 2*c + d + 2) / 4) + z.ybr[y+0][x+0] = pa + z.ybr[y+0][x+1] = pab + z.ybr[y+0][x+2] = abc + z.ybr[y+0][x+3] = bcd + z.ybr[y+1][x+0] = qp + z.ybr[y+1][x+1] = qpa + z.ybr[y+1][x+2] = pa + z.ybr[y+1][x+3] = pab + z.ybr[y+2][x+0] = rq + z.ybr[y+2][x+1] = rqp + z.ybr[y+2][x+2] = qp + z.ybr[y+2][x+3] = qpa + z.ybr[y+3][x+0] = sr + z.ybr[y+3][x+1] = srq + z.ybr[y+3][x+2] = rq + z.ybr[y+3][x+3] = rqp +} + +func predFunc4HU(z *Decoder, y, x int) { + s := int32(z.ybr[y+3][x-1]) + r := int32(z.ybr[y+2][x-1]) + q := int32(z.ybr[y+1][x-1]) + p := int32(z.ybr[y+0][x-1]) + pq := uint8((p + q + 1) / 2) + qr := uint8((q + r + 1) / 2) + rs := uint8((r + s + 1) / 2) + pqr := uint8((p + 2*q + r + 2) / 4) + qrs := uint8((q + 2*r + s + 2) / 4) + rss := uint8((r + 2*s + s + 2) / 4) + sss := uint8(s) + z.ybr[y+0][x+0] = pq + z.ybr[y+0][x+1] = pqr + z.ybr[y+0][x+2] = qr + z.ybr[y+0][x+3] = qrs + z.ybr[y+1][x+0] = qr + z.ybr[y+1][x+1] = qrs + z.ybr[y+1][x+2] = rs + z.ybr[y+1][x+3] = rss + z.ybr[y+2][x+0] = rs + z.ybr[y+2][x+1] = rss + z.ybr[y+2][x+2] = sss + z.ybr[y+2][x+3] = sss + z.ybr[y+3][x+0] = sss + z.ybr[y+3][x+1] = sss + z.ybr[y+3][x+2] = sss + z.ybr[y+3][x+3] = sss +} + +func predFunc8DC(z *Decoder, y, x int) { + sum := uint32(8) + for i := 0; i < 8; i++ { + sum += uint32(z.ybr[y-1][x+i]) + } + for j := 0; j < 8; j++ { + sum += uint32(z.ybr[y+j][x-1]) + } + avg := uint8(sum / 16) + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc8TM(z *Decoder, y, x int) { + delta0 := -int32(z.ybr[y-1][x-1]) + for j := 0; j < 8; j++ { + delta1 := delta0 + int32(z.ybr[y+j][x-1]) + for i := 0; i < 8; i++ { + delta2 := delta1 + int32(z.ybr[y-1][x+i]) + z.ybr[y+j][x+i] = uint8(clip(delta2, 0, 255)) + } + } +} + +func predFunc8VE(z *Decoder, y, x int) { + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = z.ybr[y-1][x+i] + } + } +} + +func predFunc8HE(z *Decoder, y, x int) { + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = z.ybr[y+j][x-1] + } + } +} + +func predFunc8DCTop(z *Decoder, y, x int) { + sum := uint32(4) + for j := 0; j < 8; j++ { + sum += uint32(z.ybr[y+j][x-1]) + } + avg := uint8(sum / 8) + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc8DCLeft(z *Decoder, y, x int) { + sum := uint32(4) + for i := 0; i < 8; i++ { + sum += uint32(z.ybr[y-1][x+i]) + } + avg := uint8(sum / 8) + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc8DCTopLeft(z *Decoder, y, x int) { + for j := 0; j < 8; j++ { + for i := 0; i < 8; i++ { + z.ybr[y+j][x+i] = 0x80 + } + } +} + +func predFunc16DC(z *Decoder, y, x int) { + sum := uint32(16) + for i := 0; i < 16; i++ { + sum += uint32(z.ybr[y-1][x+i]) + } + for j := 0; j < 16; j++ { + sum += uint32(z.ybr[y+j][x-1]) + } + avg := uint8(sum / 32) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc16TM(z *Decoder, y, x int) { + delta0 := -int32(z.ybr[y-1][x-1]) + for j := 0; j < 16; j++ { + delta1 := delta0 + int32(z.ybr[y+j][x-1]) + for i := 0; i < 16; i++ { + delta2 := delta1 + int32(z.ybr[y-1][x+i]) + z.ybr[y+j][x+i] = uint8(clip(delta2, 0, 255)) + } + } +} + +func predFunc16VE(z *Decoder, y, x int) { + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = z.ybr[y-1][x+i] + } + } +} + +func predFunc16HE(z *Decoder, y, x int) { + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = z.ybr[y+j][x-1] + } + } +} + +func predFunc16DCTop(z *Decoder, y, x int) { + sum := uint32(8) + for j := 0; j < 16; j++ { + sum += uint32(z.ybr[y+j][x-1]) + } + avg := uint8(sum / 16) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc16DCLeft(z *Decoder, y, x int) { + sum := uint32(8) + for i := 0; i < 16; i++ { + sum += uint32(z.ybr[y-1][x+i]) + } + avg := uint8(sum / 16) + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = avg + } + } +} + +func predFunc16DCTopLeft(z *Decoder, y, x int) { + for j := 0; j < 16; j++ { + for i := 0; i < 16; i++ { + z.ybr[y+j][x+i] = 0x80 + } + } +} diff --git a/vendor/golang.org/x/image/vp8/quant.go b/vendor/golang.org/x/image/vp8/quant.go new file mode 100644 index 00000000..da436160 --- /dev/null +++ b/vendor/golang.org/x/image/vp8/quant.go @@ -0,0 +1,98 @@ +// Copyright 2011 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. + +package vp8 + +// This file implements parsing the quantization factors. + +// quant are DC/AC quantization factors. +type quant struct { + y1 [2]uint16 + y2 [2]uint16 + uv [2]uint16 +} + +// clip clips x to the range [min, max] inclusive. +func clip(x, min, max int32) int32 { + if x < min { + return min + } + if x > max { + return max + } + return x +} + +// parseQuant parses the quantization factors, as specified in section 9.6. +func (d *Decoder) parseQuant() { + baseQ0 := d.fp.readUint(uniformProb, 7) + dqy1DC := d.fp.readOptionalInt(uniformProb, 4) + const dqy1AC = 0 + dqy2DC := d.fp.readOptionalInt(uniformProb, 4) + dqy2AC := d.fp.readOptionalInt(uniformProb, 4) + dquvDC := d.fp.readOptionalInt(uniformProb, 4) + dquvAC := d.fp.readOptionalInt(uniformProb, 4) + for i := 0; i < nSegment; i++ { + q := int32(baseQ0) + if d.segmentHeader.useSegment { + if d.segmentHeader.relativeDelta { + q += int32(d.segmentHeader.quantizer[i]) + } else { + q = int32(d.segmentHeader.quantizer[i]) + } + } + d.quant[i].y1[0] = dequantTableDC[clip(q+dqy1DC, 0, 127)] + d.quant[i].y1[1] = dequantTableAC[clip(q+dqy1AC, 0, 127)] + d.quant[i].y2[0] = dequantTableDC[clip(q+dqy2DC, 0, 127)] * 2 + d.quant[i].y2[1] = dequantTableAC[clip(q+dqy2AC, 0, 127)] * 155 / 100 + if d.quant[i].y2[1] < 8 { + d.quant[i].y2[1] = 8 + } + // The 117 is not a typo. The dequant_init function in the spec's Reference + // Decoder Source Code (http://tools.ietf.org/html/rfc6386#section-9.6 Page 145) + // says to clamp the LHS value at 132, which is equal to dequantTableDC[117]. + d.quant[i].uv[0] = dequantTableDC[clip(q+dquvDC, 0, 117)] + d.quant[i].uv[1] = dequantTableAC[clip(q+dquvAC, 0, 127)] + } +} + +// The dequantization tables are specified in section 14.1. +var ( + dequantTableDC = [128]uint16{ + 4, 5, 6, 7, 8, 9, 10, 10, + 11, 12, 13, 14, 15, 16, 17, 17, + 18, 19, 20, 20, 21, 21, 22, 22, + 23, 23, 24, 25, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, + 37, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 76, 77, 78, 79, 80, 81, + 82, 83, 84, 85, 86, 87, 88, 89, + 91, 93, 95, 96, 98, 100, 101, 102, + 104, 106, 108, 110, 112, 114, 116, 118, + 122, 124, 126, 128, 130, 132, 134, 136, + 138, 140, 143, 145, 148, 151, 154, 157, + } + dequantTableAC = [128]uint16{ + 4, 5, 6, 7, 8, 9, 10, 11, + 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, 26, 27, + 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, + 44, 45, 46, 47, 48, 49, 50, 51, + 52, 53, 54, 55, 56, 57, 58, 60, + 62, 64, 66, 68, 70, 72, 74, 76, + 78, 80, 82, 84, 86, 88, 90, 92, + 94, 96, 98, 100, 102, 104, 106, 108, + 110, 112, 114, 116, 119, 122, 125, 128, + 131, 134, 137, 140, 143, 146, 149, 152, + 155, 158, 161, 164, 167, 170, 173, 177, + 181, 185, 189, 193, 197, 201, 205, 209, + 213, 217, 221, 225, 229, 234, 239, 245, + 249, 254, 259, 264, 269, 274, 279, 284, + } +) diff --git a/vendor/golang.org/x/image/vp8/reconstruct.go b/vendor/golang.org/x/image/vp8/reconstruct.go new file mode 100644 index 00000000..c1cc4b53 --- /dev/null +++ b/vendor/golang.org/x/image/vp8/reconstruct.go @@ -0,0 +1,442 @@ +// Copyright 2011 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. + +package vp8 + +// This file implements decoding DCT/WHT residual coefficients and +// reconstructing YCbCr data equal to predicted values plus residuals. +// +// There are 1*16*16 + 2*8*8 + 1*4*4 coefficients per macroblock: +// - 1*16*16 luma DCT coefficients, +// - 2*8*8 chroma DCT coefficients, and +// - 1*4*4 luma WHT coefficients. +// Coefficients are read in lots of 16, and the later coefficients in each lot +// are often zero. +// +// The YCbCr data consists of 1*16*16 luma values and 2*8*8 chroma values, +// plus previously decoded values along the top and left borders. The combined +// values are laid out as a [1+16+1+8][32]uint8 so that vertically adjacent +// samples are 32 bytes apart. In detail, the layout is: +// +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// . . . . . . . a b b b b b b b b b b b b b b b b c c c c . . . . 0 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 1 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 2 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 3 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y c c c c . . . . 4 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 5 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 6 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 7 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y c c c c . . . . 8 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 9 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 10 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 11 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y c c c c . . . . 12 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 13 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 14 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 15 +// . . . . . . . d Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . 16 +// . . . . . . . e f f f f f f f f . . . . . . . g h h h h h h h h 17 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 18 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 19 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 20 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 21 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 22 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 23 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 24 +// . . . . . . . i B B B B B B B B . . . . . . . j R R R R R R R R 25 +// +// Y, B and R are the reconstructed luma (Y) and chroma (B, R) values. +// The Y values are predicted (either as one 16x16 region or 16 4x4 regions) +// based on the row above's Y values (some combination of {abc} or {dYC}) and +// the column left's Y values (either {ad} or {bY}). Similarly, B and R values +// are predicted on the row above and column left of their respective 8x8 +// region: {efi} for B, {ghj} for R. +// +// For uppermost macroblocks (i.e. those with mby == 0), the {abcefgh} values +// are initialized to 0x81. Otherwise, they are copied from the bottom row of +// the macroblock above. The {c} values are then duplicated from row 0 to rows +// 4, 8 and 12 of the ybr workspace. +// Similarly, for leftmost macroblocks (i.e. those with mbx == 0), the {adeigj} +// values are initialized to 0x7f. Otherwise, they are copied from the right +// column of the macroblock to the left. +// For the top-left macroblock (with mby == 0 && mbx == 0), {aeg} is 0x81. +// +// When moving from one macroblock to the next horizontally, the {adeigj} +// values can simply be copied from the workspace to itself, shifted by 8 or +// 16 columns. When moving from one macroblock to the next vertically, +// filtering can occur and hence the row values have to be copied from the +// post-filtered image instead of the pre-filtered workspace. + +const ( + bCoeffBase = 1*16*16 + 0*8*8 + rCoeffBase = 1*16*16 + 1*8*8 + whtCoeffBase = 1*16*16 + 2*8*8 +) + +const ( + ybrYX = 8 + ybrYY = 1 + ybrBX = 8 + ybrBY = 18 + ybrRX = 24 + ybrRY = 18 +) + +// prepareYBR prepares the {abcdefghij} elements of ybr. +func (d *Decoder) prepareYBR(mbx, mby int) { + if mbx == 0 { + for y := 0; y < 17; y++ { + d.ybr[y][7] = 0x81 + } + for y := 17; y < 26; y++ { + d.ybr[y][7] = 0x81 + d.ybr[y][23] = 0x81 + } + } else { + for y := 0; y < 17; y++ { + d.ybr[y][7] = d.ybr[y][7+16] + } + for y := 17; y < 26; y++ { + d.ybr[y][7] = d.ybr[y][15] + d.ybr[y][23] = d.ybr[y][31] + } + } + if mby == 0 { + for x := 7; x < 28; x++ { + d.ybr[0][x] = 0x7f + } + for x := 7; x < 16; x++ { + d.ybr[17][x] = 0x7f + } + for x := 23; x < 32; x++ { + d.ybr[17][x] = 0x7f + } + } else { + for i := 0; i < 16; i++ { + d.ybr[0][8+i] = d.img.Y[(16*mby-1)*d.img.YStride+16*mbx+i] + } + for i := 0; i < 8; i++ { + d.ybr[17][8+i] = d.img.Cb[(8*mby-1)*d.img.CStride+8*mbx+i] + } + for i := 0; i < 8; i++ { + d.ybr[17][24+i] = d.img.Cr[(8*mby-1)*d.img.CStride+8*mbx+i] + } + if mbx == d.mbw-1 { + for i := 16; i < 20; i++ { + d.ybr[0][8+i] = d.img.Y[(16*mby-1)*d.img.YStride+16*mbx+15] + } + } else { + for i := 16; i < 20; i++ { + d.ybr[0][8+i] = d.img.Y[(16*mby-1)*d.img.YStride+16*mbx+i] + } + } + } + for y := 4; y < 16; y += 4 { + d.ybr[y][24] = d.ybr[0][24] + d.ybr[y][25] = d.ybr[0][25] + d.ybr[y][26] = d.ybr[0][26] + d.ybr[y][27] = d.ybr[0][27] + } +} + +// btou converts a bool to a 0/1 value. +func btou(b bool) uint8 { + if b { + return 1 + } + return 0 +} + +// pack packs four 0/1 values into four bits of a uint32. +func pack(x [4]uint8, shift int) uint32 { + u := uint32(x[0])<<0 | uint32(x[1])<<1 | uint32(x[2])<<2 | uint32(x[3])<<3 + return u << uint(shift) +} + +// unpack unpacks four 0/1 values from a four-bit value. +var unpack = [16][4]uint8{ + {0, 0, 0, 0}, + {1, 0, 0, 0}, + {0, 1, 0, 0}, + {1, 1, 0, 0}, + {0, 0, 1, 0}, + {1, 0, 1, 0}, + {0, 1, 1, 0}, + {1, 1, 1, 0}, + {0, 0, 0, 1}, + {1, 0, 0, 1}, + {0, 1, 0, 1}, + {1, 1, 0, 1}, + {0, 0, 1, 1}, + {1, 0, 1, 1}, + {0, 1, 1, 1}, + {1, 1, 1, 1}, +} + +var ( + // The mapping from 4x4 region position to band is specified in section 13.3. + bands = [17]uint8{0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0} + // Category probabilties are specified in section 13.2. + // Decoding categories 1 and 2 are done inline. + cat3456 = [4][12]uint8{ + {173, 148, 140, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {176, 155, 140, 135, 0, 0, 0, 0, 0, 0, 0, 0}, + {180, 157, 141, 134, 130, 0, 0, 0, 0, 0, 0, 0}, + {254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129, 0}, + } + // The zigzag order is: + // 0 1 5 6 + // 2 4 7 12 + // 3 8 11 13 + // 9 10 14 15 + zigzag = [16]uint8{0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15} +) + +// parseResiduals4 parses a 4x4 region of residual coefficients, as specified +// in section 13.3, and returns a 0/1 value indicating whether there was at +// least one non-zero coefficient. +// r is the partition to read bits from. +// plane and context describe which token probability table to use. context is +// either 0, 1 or 2, and equals how many of the macroblock left and macroblock +// above have non-zero coefficients. +// quant are the DC/AC quantization factors. +// skipFirstCoeff is whether the DC coefficient has already been parsed. +// coeffBase is the base index of d.coeff to write to. +func (d *Decoder) parseResiduals4(r *partition, plane int, context uint8, quant [2]uint16, skipFirstCoeff bool, coeffBase int) uint8 { + prob, n := &d.tokenProb[plane], 0 + if skipFirstCoeff { + n = 1 + } + p := prob[bands[n]][context] + if !r.readBit(p[0]) { + return 0 + } + for n != 16 { + n++ + if !r.readBit(p[1]) { + p = prob[bands[n]][0] + continue + } + var v uint32 + if !r.readBit(p[2]) { + v = 1 + p = prob[bands[n]][1] + } else { + if !r.readBit(p[3]) { + if !r.readBit(p[4]) { + v = 2 + } else { + v = 3 + r.readUint(p[5], 1) + } + } else if !r.readBit(p[6]) { + if !r.readBit(p[7]) { + // Category 1. + v = 5 + r.readUint(159, 1) + } else { + // Category 2. + v = 7 + 2*r.readUint(165, 1) + r.readUint(145, 1) + } + } else { + // Categories 3, 4, 5 or 6. + b1 := r.readUint(p[8], 1) + b0 := r.readUint(p[9+b1], 1) + cat := 2*b1 + b0 + tab := &cat3456[cat] + v = 0 + for i := 0; tab[i] != 0; i++ { + v *= 2 + v += r.readUint(tab[i], 1) + } + v += 3 + (8 << cat) + } + p = prob[bands[n]][2] + } + z := zigzag[n-1] + c := int32(v) * int32(quant[btou(z > 0)]) + if r.readBit(uniformProb) { + c = -c + } + d.coeff[coeffBase+int(z)] = int16(c) + if n == 16 || !r.readBit(p[0]) { + return 1 + } + } + return 1 +} + +// parseResiduals parses the residuals and returns whether inner loop filtering +// should be skipped for this macroblock. +func (d *Decoder) parseResiduals(mbx, mby int) (skip bool) { + partition := &d.op[mby&(d.nOP-1)] + plane := planeY1SansY2 + quant := &d.quant[d.segment] + + // Parse the DC coefficient of each 4x4 luma region. + if d.usePredY16 { + nz := d.parseResiduals4(partition, planeY2, d.leftMB.nzY16+d.upMB[mbx].nzY16, quant.y2, false, whtCoeffBase) + d.leftMB.nzY16 = nz + d.upMB[mbx].nzY16 = nz + d.inverseWHT16() + plane = planeY1WithY2 + } + + var ( + nzDC, nzAC [4]uint8 + nzDCMask, nzACMask uint32 + coeffBase int + ) + + // Parse the luma coefficients. + lnz := unpack[d.leftMB.nzMask&0x0f] + unz := unpack[d.upMB[mbx].nzMask&0x0f] + for y := 0; y < 4; y++ { + nz := lnz[y] + for x := 0; x < 4; x++ { + nz = d.parseResiduals4(partition, plane, nz+unz[x], quant.y1, d.usePredY16, coeffBase) + unz[x] = nz + nzAC[x] = nz + nzDC[x] = btou(d.coeff[coeffBase] != 0) + coeffBase += 16 + } + lnz[y] = nz + nzDCMask |= pack(nzDC, y*4) + nzACMask |= pack(nzAC, y*4) + } + lnzMask := pack(lnz, 0) + unzMask := pack(unz, 0) + + // Parse the chroma coefficients. + lnz = unpack[d.leftMB.nzMask>>4] + unz = unpack[d.upMB[mbx].nzMask>>4] + for c := 0; c < 4; c += 2 { + for y := 0; y < 2; y++ { + nz := lnz[y+c] + for x := 0; x < 2; x++ { + nz = d.parseResiduals4(partition, planeUV, nz+unz[x+c], quant.uv, false, coeffBase) + unz[x+c] = nz + nzAC[y*2+x] = nz + nzDC[y*2+x] = btou(d.coeff[coeffBase] != 0) + coeffBase += 16 + } + lnz[y+c] = nz + } + nzDCMask |= pack(nzDC, 16+c*2) + nzACMask |= pack(nzAC, 16+c*2) + } + lnzMask |= pack(lnz, 4) + unzMask |= pack(unz, 4) + + // Save decoder state. + d.leftMB.nzMask = uint8(lnzMask) + d.upMB[mbx].nzMask = uint8(unzMask) + d.nzDCMask = nzDCMask + d.nzACMask = nzACMask + + // Section 15.1 of the spec says that "Steps 2 and 4 [of the loop filter] + // are skipped... [if] there is no DCT coefficient coded for the whole + // macroblock." + return nzDCMask == 0 && nzACMask == 0 +} + +// reconstructMacroblock applies the predictor functions and adds the inverse- +// DCT transformed residuals to recover the YCbCr data. +func (d *Decoder) reconstructMacroblock(mbx, mby int) { + if d.usePredY16 { + p := checkTopLeftPred(mbx, mby, d.predY16) + predFunc16[p](d, 1, 8) + for j := 0; j < 4; j++ { + for i := 0; i < 4; i++ { + n := 4*j + i + y := 4*j + 1 + x := 4*i + 8 + mask := uint32(1) << uint(n) + if d.nzACMask&mask != 0 { + d.inverseDCT4(y, x, 16*n) + } else if d.nzDCMask&mask != 0 { + d.inverseDCT4DCOnly(y, x, 16*n) + } + } + } + } else { + for j := 0; j < 4; j++ { + for i := 0; i < 4; i++ { + n := 4*j + i + y := 4*j + 1 + x := 4*i + 8 + predFunc4[d.predY4[j][i]](d, y, x) + mask := uint32(1) << uint(n) + if d.nzACMask&mask != 0 { + d.inverseDCT4(y, x, 16*n) + } else if d.nzDCMask&mask != 0 { + d.inverseDCT4DCOnly(y, x, 16*n) + } + } + } + } + p := checkTopLeftPred(mbx, mby, d.predC8) + predFunc8[p](d, ybrBY, ybrBX) + if d.nzACMask&0x0f0000 != 0 { + d.inverseDCT8(ybrBY, ybrBX, bCoeffBase) + } else if d.nzDCMask&0x0f0000 != 0 { + d.inverseDCT8DCOnly(ybrBY, ybrBX, bCoeffBase) + } + predFunc8[p](d, ybrRY, ybrRX) + if d.nzACMask&0xf00000 != 0 { + d.inverseDCT8(ybrRY, ybrRX, rCoeffBase) + } else if d.nzDCMask&0xf00000 != 0 { + d.inverseDCT8DCOnly(ybrRY, ybrRX, rCoeffBase) + } +} + +// reconstruct reconstructs one macroblock and returns whether inner loop +// filtering should be skipped for it. +func (d *Decoder) reconstruct(mbx, mby int) (skip bool) { + if d.segmentHeader.updateMap { + if !d.fp.readBit(d.segmentHeader.prob[0]) { + d.segment = int(d.fp.readUint(d.segmentHeader.prob[1], 1)) + } else { + d.segment = int(d.fp.readUint(d.segmentHeader.prob[2], 1)) + 2 + } + } + if d.useSkipProb { + skip = d.fp.readBit(d.skipProb) + } + // Prepare the workspace. + for i := range d.coeff { + d.coeff[i] = 0 + } + d.prepareYBR(mbx, mby) + // Parse the predictor modes. + d.usePredY16 = d.fp.readBit(145) + if d.usePredY16 { + d.parsePredModeY16(mbx) + } else { + d.parsePredModeY4(mbx) + } + d.parsePredModeC8() + // Parse the residuals. + if !skip { + skip = d.parseResiduals(mbx, mby) + } else { + if d.usePredY16 { + d.leftMB.nzY16 = 0 + d.upMB[mbx].nzY16 = 0 + } + d.leftMB.nzMask = 0 + d.upMB[mbx].nzMask = 0 + d.nzDCMask = 0 + d.nzACMask = 0 + } + // Reconstruct the YCbCr data and copy it to the image. + d.reconstructMacroblock(mbx, mby) + for i, y := (mby*d.img.YStride+mbx)*16, 0; y < 16; i, y = i+d.img.YStride, y+1 { + copy(d.img.Y[i:i+16], d.ybr[ybrYY+y][ybrYX:ybrYX+16]) + } + for i, y := (mby*d.img.CStride+mbx)*8, 0; y < 8; i, y = i+d.img.CStride, y+1 { + copy(d.img.Cb[i:i+8], d.ybr[ybrBY+y][ybrBX:ybrBX+8]) + copy(d.img.Cr[i:i+8], d.ybr[ybrRY+y][ybrRX:ybrRX+8]) + } + return skip +} diff --git a/vendor/golang.org/x/image/vp8/token.go b/vendor/golang.org/x/image/vp8/token.go new file mode 100644 index 00000000..da99cf0f --- /dev/null +++ b/vendor/golang.org/x/image/vp8/token.go @@ -0,0 +1,381 @@ +// Copyright 2011 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. + +package vp8 + +// This file contains token probabilities for decoding DCT/WHT coefficients, as +// specified in chapter 13. + +func (d *Decoder) parseTokenProb() { + for i := range d.tokenProb { + for j := range d.tokenProb[i] { + for k := range d.tokenProb[i][j] { + for l := range d.tokenProb[i][j][k] { + if d.fp.readBit(tokenProbUpdateProb[i][j][k][l]) { + d.tokenProb[i][j][k][l] = uint8(d.fp.readUint(uniformProb, 8)) + } + } + } + } + } +} + +// The plane enumeration is specified in section 13.3. +const ( + planeY1WithY2 = iota + planeY2 + planeUV + planeY1SansY2 + nPlane +) + +const ( + nBand = 8 + nContext = 3 + nProb = 11 +) + +// Token probability update probabilities are specified in section 13.4. +var tokenProbUpdateProb = [nPlane][nBand][nContext][nProb]uint8{ + { + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255}, + {249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255}, + {234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255}, + {250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255}, + {254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + }, + { + { + {217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255}, + {234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255}, + }, + { + {255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + }, + { + { + {186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255}, + {234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255}, + {251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255}, + }, + { + {255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + }, + { + { + {248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255}, + {248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255}, + {248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255}, + {250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + { + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255}, + }, + }, +} + +// Default token probabilities are specified in section 13.5. +var defaultTokenProb = [nPlane][nBand][nContext][nProb]uint8{ + { + { + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + { + {253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128}, + {189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128}, + {106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128}, + }, + { + {1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128}, + {181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128}, + {78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128}, + }, + { + {1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128}, + {184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128}, + {77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128}, + }, + { + {1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128}, + {170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128}, + {37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128}, + }, + { + {1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128}, + {207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128}, + {102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128}, + }, + { + {1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128}, + {177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128}, + {80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128}, + }, + { + {1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + }, + { + { + {198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62}, + {131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1}, + {68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128}, + }, + { + {1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128}, + {184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128}, + {81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128}, + }, + { + {1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128}, + {99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128}, + {23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128}, + }, + { + {1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128}, + {109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128}, + {44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128}, + }, + { + {1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128}, + {94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128}, + {22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128}, + }, + { + {1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128}, + {124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128}, + {35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128}, + }, + { + {1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128}, + {121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128}, + {45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128}, + }, + { + {1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128}, + {203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128}, + {137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128}, + }, + }, + { + { + {253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128}, + {175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128}, + {73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128}, + }, + { + {1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128}, + {239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128}, + {155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128}, + }, + { + {1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128}, + {201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128}, + {69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128}, + }, + { + {1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128}, + {223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128}, + {141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128}, + }, + { + {1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128}, + {190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128}, + {149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + { + {1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + { + {1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128}, + {213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128}, + {55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + { + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + {128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + }, + { + { + {202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255}, + {126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128}, + {61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128}, + }, + { + {1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128}, + {166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128}, + {39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128}, + }, + { + {1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128}, + {124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128}, + {24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128}, + }, + { + {1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128}, + {149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128}, + {28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128}, + }, + { + {1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128}, + {123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128}, + {20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128}, + }, + { + {1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128}, + {168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128}, + {47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128}, + }, + { + {1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128}, + {141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128}, + {42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128}, + }, + { + {1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + {238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128}, + }, + }, +} diff --git a/vendor/golang.org/x/image/vp8l/decode.go b/vendor/golang.org/x/image/vp8l/decode.go new file mode 100644 index 00000000..43194870 --- /dev/null +++ b/vendor/golang.org/x/image/vp8l/decode.go @@ -0,0 +1,603 @@ +// Copyright 2014 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. + +// Package vp8l implements a decoder for the VP8L lossless image format. +// +// The VP8L specification is at: +// https://developers.google.com/speed/webp/docs/riff_container +package vp8l // import "golang.org/x/image/vp8l" + +import ( + "bufio" + "errors" + "image" + "image/color" + "io" +) + +var ( + errInvalidCodeLengths = errors.New("vp8l: invalid code lengths") + errInvalidHuffmanTree = errors.New("vp8l: invalid Huffman tree") +) + +// colorCacheMultiplier is the multiplier used for the color cache hash +// function, specified in section 4.2.3. +const colorCacheMultiplier = 0x1e35a7bd + +// distanceMapTable is the look-up table for distanceMap. +var distanceMapTable = [120]uint8{ + 0x18, 0x07, 0x17, 0x19, 0x28, 0x06, 0x27, 0x29, 0x16, 0x1a, + 0x26, 0x2a, 0x38, 0x05, 0x37, 0x39, 0x15, 0x1b, 0x36, 0x3a, + 0x25, 0x2b, 0x48, 0x04, 0x47, 0x49, 0x14, 0x1c, 0x35, 0x3b, + 0x46, 0x4a, 0x24, 0x2c, 0x58, 0x45, 0x4b, 0x34, 0x3c, 0x03, + 0x57, 0x59, 0x13, 0x1d, 0x56, 0x5a, 0x23, 0x2d, 0x44, 0x4c, + 0x55, 0x5b, 0x33, 0x3d, 0x68, 0x02, 0x67, 0x69, 0x12, 0x1e, + 0x66, 0x6a, 0x22, 0x2e, 0x54, 0x5c, 0x43, 0x4d, 0x65, 0x6b, + 0x32, 0x3e, 0x78, 0x01, 0x77, 0x79, 0x53, 0x5d, 0x11, 0x1f, + 0x64, 0x6c, 0x42, 0x4e, 0x76, 0x7a, 0x21, 0x2f, 0x75, 0x7b, + 0x31, 0x3f, 0x63, 0x6d, 0x52, 0x5e, 0x00, 0x74, 0x7c, 0x41, + 0x4f, 0x10, 0x20, 0x62, 0x6e, 0x30, 0x73, 0x7d, 0x51, 0x5f, + 0x40, 0x72, 0x7e, 0x61, 0x6f, 0x50, 0x71, 0x7f, 0x60, 0x70, +} + +// distanceMap maps a LZ77 backwards reference distance to a two-dimensional +// pixel offset, specified in section 4.2.2. +func distanceMap(w int32, code uint32) int32 { + if int32(code) > int32(len(distanceMapTable)) { + return int32(code) - int32(len(distanceMapTable)) + } + distCode := int32(distanceMapTable[code-1]) + yOffset := distCode >> 4 + xOffset := 8 - distCode&0xf + if d := yOffset*w + xOffset; d >= 1 { + return d + } + return 1 +} + +// decoder holds the bit-stream for a VP8L image. +type decoder struct { + r io.ByteReader + bits uint32 + nBits uint32 +} + +// read reads the next n bits from the decoder's bit-stream. +func (d *decoder) read(n uint32) (uint32, error) { + for d.nBits < n { + c, err := d.r.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return 0, err + } + d.bits |= uint32(c) << d.nBits + d.nBits += 8 + } + u := d.bits & (1<>= n + d.nBits -= n + return u, nil +} + +// decodeTransform decodes the next transform and the width of the image after +// transformation (or equivalently, before inverse transformation), specified +// in section 3. +func (d *decoder) decodeTransform(w int32, h int32) (t transform, newWidth int32, err error) { + t.oldWidth = w + t.transformType, err = d.read(2) + if err != nil { + return transform{}, 0, err + } + switch t.transformType { + case transformTypePredictor, transformTypeCrossColor: + t.bits, err = d.read(3) + if err != nil { + return transform{}, 0, err + } + t.bits += 2 + t.pix, err = d.decodePix(nTiles(w, t.bits), nTiles(h, t.bits), 0, false) + if err != nil { + return transform{}, 0, err + } + case transformTypeSubtractGreen: + // No-op. + case transformTypeColorIndexing: + nColors, err := d.read(8) + if err != nil { + return transform{}, 0, err + } + nColors++ + t.bits = 0 + switch { + case nColors <= 2: + t.bits = 3 + case nColors <= 4: + t.bits = 2 + case nColors <= 16: + t.bits = 1 + } + w = nTiles(w, t.bits) + pix, err := d.decodePix(int32(nColors), 1, 4*256, false) + if err != nil { + return transform{}, 0, err + } + for p := 4; p < len(pix); p += 4 { + pix[p+0] += pix[p-4] + pix[p+1] += pix[p-3] + pix[p+2] += pix[p-2] + pix[p+3] += pix[p-1] + } + // The spec says that "if the index is equal or larger than color_table_size, + // the argb color value should be set to 0x00000000 (transparent black)." + // We re-slice up to 256 4-byte pixels. + t.pix = pix[:4*256] + } + return t, w, nil +} + +// repeatsCodeLength is the minimum code length for repeated codes. +const repeatsCodeLength = 16 + +// These magic numbers are specified at the end of section 5.2.2. +// The 3-length arrays apply to code lengths >= repeatsCodeLength. +var ( + codeLengthCodeOrder = [19]uint8{ + 17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, + } + repeatBits = [3]uint8{2, 3, 7} + repeatOffsets = [3]uint8{3, 3, 11} +) + +// decodeCodeLengths decodes a Huffman tree's code lengths which are themselves +// encoded via a Huffman tree, specified in section 5.2.2. +func (d *decoder) decodeCodeLengths(dst []uint32, codeLengthCodeLengths []uint32) error { + h := hTree{} + if err := h.build(codeLengthCodeLengths); err != nil { + return err + } + + maxSymbol := len(dst) + useLength, err := d.read(1) + if err != nil { + return err + } + if useLength != 0 { + n, err := d.read(3) + if err != nil { + return err + } + n = 2 + 2*n + ms, err := d.read(n) + if err != nil { + return err + } + maxSymbol = int(ms) + 2 + if maxSymbol > len(dst) { + return errInvalidCodeLengths + } + } + + // The spec says that "if code 16 [meaning repeat] is used before + // a non-zero value has been emitted, a value of 8 is repeated." + prevCodeLength := uint32(8) + + for symbol := 0; symbol < len(dst); { + if maxSymbol == 0 { + break + } + maxSymbol-- + codeLength, err := h.next(d) + if err != nil { + return err + } + if codeLength < repeatsCodeLength { + dst[symbol] = codeLength + symbol++ + if codeLength != 0 { + prevCodeLength = codeLength + } + continue + } + + repeat, err := d.read(uint32(repeatBits[codeLength-repeatsCodeLength])) + if err != nil { + return err + } + repeat += uint32(repeatOffsets[codeLength-repeatsCodeLength]) + if symbol+int(repeat) > len(dst) { + return errInvalidCodeLengths + } + // A code length of 16 repeats the previous non-zero code. + // A code length of 17 or 18 repeats zeroes. + cl := uint32(0) + if codeLength == 16 { + cl = prevCodeLength + } + for ; repeat > 0; repeat-- { + dst[symbol] = cl + symbol++ + } + } + return nil +} + +// decodeHuffmanTree decodes a Huffman tree into h. +func (d *decoder) decodeHuffmanTree(h *hTree, alphabetSize uint32) error { + useSimple, err := d.read(1) + if err != nil { + return err + } + if useSimple != 0 { + nSymbols, err := d.read(1) + if err != nil { + return err + } + nSymbols++ + firstSymbolLengthCode, err := d.read(1) + if err != nil { + return err + } + firstSymbolLengthCode = 7*firstSymbolLengthCode + 1 + var symbols [2]uint32 + symbols[0], err = d.read(firstSymbolLengthCode) + if err != nil { + return err + } + if nSymbols == 2 { + symbols[1], err = d.read(8) + if err != nil { + return err + } + } + return h.buildSimple(nSymbols, symbols, alphabetSize) + } + + nCodes, err := d.read(4) + if err != nil { + return err + } + nCodes += 4 + if int(nCodes) > len(codeLengthCodeOrder) { + return errInvalidHuffmanTree + } + codeLengthCodeLengths := [len(codeLengthCodeOrder)]uint32{} + for i := uint32(0); i < nCodes; i++ { + codeLengthCodeLengths[codeLengthCodeOrder[i]], err = d.read(3) + if err != nil { + return err + } + } + codeLengths := make([]uint32, alphabetSize) + if err = d.decodeCodeLengths(codeLengths, codeLengthCodeLengths[:]); err != nil { + return err + } + return h.build(codeLengths) +} + +const ( + huffGreen = 0 + huffRed = 1 + huffBlue = 2 + huffAlpha = 3 + huffDistance = 4 + nHuff = 5 +) + +// hGroup is an array of 5 Huffman trees. +type hGroup [nHuff]hTree + +// decodeHuffmanGroups decodes the one or more hGroups used to decode the pixel +// data. If one hGroup is used for the entire image, then hPix and hBits will +// be zero. If more than one hGroup is used, then hPix contains the meta-image +// that maps tiles to hGroup index, and hBits contains the log-2 tile size. +func (d *decoder) decodeHuffmanGroups(w int32, h int32, topLevel bool, ccBits uint32) ( + hGroups []hGroup, hPix []byte, hBits uint32, err error) { + + maxHGroupIndex := 0 + if topLevel { + useMeta, err := d.read(1) + if err != nil { + return nil, nil, 0, err + } + if useMeta != 0 { + hBits, err = d.read(3) + if err != nil { + return nil, nil, 0, err + } + hBits += 2 + hPix, err = d.decodePix(nTiles(w, hBits), nTiles(h, hBits), 0, false) + if err != nil { + return nil, nil, 0, err + } + for p := 0; p < len(hPix); p += 4 { + i := int(hPix[p])<<8 | int(hPix[p+1]) + if maxHGroupIndex < i { + maxHGroupIndex = i + } + } + } + } + hGroups = make([]hGroup, maxHGroupIndex+1) + for i := range hGroups { + for j, alphabetSize := range alphabetSizes { + if j == 0 && ccBits > 0 { + alphabetSize += 1 << ccBits + } + if err := d.decodeHuffmanTree(&hGroups[i][j], alphabetSize); err != nil { + return nil, nil, 0, err + } + } + } + return hGroups, hPix, hBits, nil +} + +const ( + nLiteralCodes = 256 + nLengthCodes = 24 + nDistanceCodes = 40 +) + +var alphabetSizes = [nHuff]uint32{ + nLiteralCodes + nLengthCodes, + nLiteralCodes, + nLiteralCodes, + nLiteralCodes, + nDistanceCodes, +} + +// decodePix decodes pixel data, specified in section 5.2.2. +func (d *decoder) decodePix(w int32, h int32, minCap int32, topLevel bool) ([]byte, error) { + // Decode the color cache parameters. + ccBits, ccShift, ccEntries := uint32(0), uint32(0), ([]uint32)(nil) + useColorCache, err := d.read(1) + if err != nil { + return nil, err + } + if useColorCache != 0 { + ccBits, err = d.read(4) + if err != nil { + return nil, err + } + if ccBits < 1 || 11 < ccBits { + return nil, errors.New("vp8l: invalid color cache parameters") + } + ccShift = 32 - ccBits + ccEntries = make([]uint32, 1<>hBits) + (x >> hBits)) + hg = &hGroups[uint32(hPix[i])<<8|uint32(hPix[i+1])] + } + + green, err := hg[huffGreen].next(d) + if err != nil { + return nil, err + } + switch { + case green < nLiteralCodes: + // We have a literal pixel. + red, err := hg[huffRed].next(d) + if err != nil { + return nil, err + } + blue, err := hg[huffBlue].next(d) + if err != nil { + return nil, err + } + alpha, err := hg[huffAlpha].next(d) + if err != nil { + return nil, err + } + pix[p+0] = uint8(red) + pix[p+1] = uint8(green) + pix[p+2] = uint8(blue) + pix[p+3] = uint8(alpha) + p += 4 + + x++ + if x == w { + x, y = 0, y+1 + } + lookupHG = hMask != 0 && x&hMask == 0 + + case green < nLiteralCodes+nLengthCodes: + // We have a LZ77 backwards reference. + length, err := d.lz77Param(green - nLiteralCodes) + if err != nil { + return nil, err + } + distSym, err := hg[huffDistance].next(d) + if err != nil { + return nil, err + } + distCode, err := d.lz77Param(distSym) + if err != nil { + return nil, err + } + dist := distanceMap(w, distCode) + pEnd := p + 4*int(length) + q := p - 4*int(dist) + qEnd := pEnd - 4*int(dist) + if p < 0 || len(pix) < pEnd || q < 0 || len(pix) < qEnd { + return nil, errors.New("vp8l: invalid LZ77 parameters") + } + for ; p < pEnd; p, q = p+1, q+1 { + pix[p] = pix[q] + } + + x += int32(length) + for x >= w { + x, y = x-w, y+1 + } + lookupHG = hMask != 0 + + default: + // We have a color cache lookup. First, insert previous pixels + // into the cache. Note that VP8L assumes ARGB order, but the + // Go image.RGBA type is in RGBA order. + for ; cachedP < p; cachedP += 4 { + argb := uint32(pix[cachedP+0])<<16 | + uint32(pix[cachedP+1])<<8 | + uint32(pix[cachedP+2])<<0 | + uint32(pix[cachedP+3])<<24 + ccEntries[(argb*colorCacheMultiplier)>>ccShift] = argb + } + green -= nLiteralCodes + nLengthCodes + if int(green) >= len(ccEntries) { + return nil, errors.New("vp8l: invalid color cache index") + } + argb := ccEntries[green] + pix[p+0] = uint8(argb >> 16) + pix[p+1] = uint8(argb >> 8) + pix[p+2] = uint8(argb >> 0) + pix[p+3] = uint8(argb >> 24) + p += 4 + + x++ + if x == w { + x, y = 0, y+1 + } + lookupHG = hMask != 0 && x&hMask == 0 + } + } + return pix, nil +} + +// lz77Param returns the next LZ77 parameter: a length or a distance, specified +// in section 4.2.2. +func (d *decoder) lz77Param(symbol uint32) (uint32, error) { + if symbol < 4 { + return symbol + 1, nil + } + extraBits := (symbol - 2) >> 1 + offset := (2 + symbol&1) << extraBits + n, err := d.read(extraBits) + if err != nil { + return 0, err + } + return offset + n + 1, nil +} + +// decodeHeader decodes the VP8L header from r. +func decodeHeader(r io.Reader) (d *decoder, w int32, h int32, err error) { + rr, ok := r.(io.ByteReader) + if !ok { + rr = bufio.NewReader(r) + } + d = &decoder{r: rr} + magic, err := d.read(8) + if err != nil { + return nil, 0, 0, err + } + if magic != 0x2f { + return nil, 0, 0, errors.New("vp8l: invalid header") + } + width, err := d.read(14) + if err != nil { + return nil, 0, 0, err + } + width++ + height, err := d.read(14) + if err != nil { + return nil, 0, 0, err + } + height++ + _, err = d.read(1) // Read and ignore the hasAlpha hint. + if err != nil { + return nil, 0, 0, err + } + version, err := d.read(3) + if err != nil { + return nil, 0, 0, err + } + if version != 0 { + return nil, 0, 0, errors.New("vp8l: invalid version") + } + return d, int32(width), int32(height), nil +} + +// DecodeConfig decodes the color model and dimensions of a VP8L image from r. +func DecodeConfig(r io.Reader) (image.Config, error) { + _, w, h, err := decodeHeader(r) + if err != nil { + return image.Config{}, err + } + return image.Config{ + ColorModel: color.NRGBAModel, + Width: int(w), + Height: int(h), + }, nil +} + +// Decode decodes a VP8L image from r. +func Decode(r io.Reader) (image.Image, error) { + d, w, h, err := decodeHeader(r) + if err != nil { + return nil, err + } + // Decode the transforms. + var ( + nTransforms int + transforms [nTransformTypes]transform + transformsSeen [nTransformTypes]bool + originalW = w + ) + for { + more, err := d.read(1) + if err != nil { + return nil, err + } + if more == 0 { + break + } + var t transform + t, w, err = d.decodeTransform(w, h) + if err != nil { + return nil, err + } + if transformsSeen[t.transformType] { + return nil, errors.New("vp8l: repeated transform") + } + transformsSeen[t.transformType] = true + transforms[nTransforms] = t + nTransforms++ + } + // Decode the transformed pixels. + pix, err := d.decodePix(w, h, 0, true) + if err != nil { + return nil, err + } + // Apply the inverse transformations. + for i := nTransforms - 1; i >= 0; i-- { + t := &transforms[i] + pix = inverseTransforms[t.transformType](t, pix, h) + } + return &image.NRGBA{ + Pix: pix, + Stride: 4 * int(originalW), + Rect: image.Rect(0, 0, int(originalW), int(h)), + }, nil +} diff --git a/vendor/golang.org/x/image/vp8l/huffman.go b/vendor/golang.org/x/image/vp8l/huffman.go new file mode 100644 index 00000000..36368a87 --- /dev/null +++ b/vendor/golang.org/x/image/vp8l/huffman.go @@ -0,0 +1,245 @@ +// Copyright 2014 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. + +package vp8l + +import ( + "io" +) + +// reverseBits reverses the bits in a byte. +var reverseBits = [256]uint8{ + 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, + 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, + 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, + 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, + 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, + 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, + 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, + 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, + 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, + 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, + 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, + 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, + 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, + 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, + 0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7, + 0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff, +} + +// hNode is a node in a Huffman tree. +type hNode struct { + // symbol is the symbol held by this node. + symbol uint32 + // children, if positive, is the hTree.nodes index of the first of + // this node's two children. Zero means an uninitialized node, + // and -1 means a leaf node. + children int32 +} + +const leafNode = -1 + +// lutSize is the log-2 size of an hTree's look-up table. +const lutSize, lutMask = 7, 1<<7 - 1 + +// hTree is a Huffman tree. +type hTree struct { + // nodes are the nodes of the Huffman tree. During construction, + // len(nodes) grows from 1 up to cap(nodes) by steps of two. + // After construction, len(nodes) == cap(nodes), and both equal + // 2*theNumberOfSymbols - 1. + nodes []hNode + // lut is a look-up table for walking the nodes. The x in lut[x] is + // the next lutSize bits in the bit-stream. The low 8 bits of lut[x] + // equals 1 plus the number of bits in the next code, or 0 if the + // next code requires more than lutSize bits. The high 24 bits are: + // - the symbol, if the code requires lutSize or fewer bits, or + // - the hTree.nodes index to start the tree traversal from, if + // the next code requires more than lutSize bits. + lut [1 << lutSize]uint32 +} + +// insert inserts into the hTree a symbol whose encoding is the least +// significant codeLength bits of code. +func (h *hTree) insert(symbol uint32, code uint32, codeLength uint32) error { + if symbol > 0xffff || codeLength > 0xfe { + return errInvalidHuffmanTree + } + baseCode := uint32(0) + if codeLength > lutSize { + baseCode = uint32(reverseBits[(code>>(codeLength-lutSize))&0xff]) >> (8 - lutSize) + } else { + baseCode = uint32(reverseBits[code&0xff]) >> (8 - codeLength) + for i := 0; i < 1<<(lutSize-codeLength); i++ { + h.lut[baseCode|uint32(i)< 0; { + codeLength-- + if int(n) > len(h.nodes) { + return errInvalidHuffmanTree + } + switch h.nodes[n].children { + case leafNode: + return errInvalidHuffmanTree + case 0: + if len(h.nodes) == cap(h.nodes) { + return errInvalidHuffmanTree + } + // Create two empty child nodes. + h.nodes[n].children = int32(len(h.nodes)) + h.nodes = h.nodes[:len(h.nodes)+2] + } + n = uint32(h.nodes[n].children) + 1&(code>>codeLength) + jump-- + if jump == 0 && h.lut[baseCode] == 0 { + h.lut[baseCode] = n << 8 + } + } + + switch h.nodes[n].children { + case leafNode: + // No-op. + case 0: + // Turn the uninitialized node into a leaf. + h.nodes[n].children = leafNode + default: + return errInvalidHuffmanTree + } + h.nodes[n].symbol = symbol + return nil +} + +// codeLengthsToCodes returns the canonical Huffman codes implied by the +// sequence of code lengths. +func codeLengthsToCodes(codeLengths []uint32) ([]uint32, error) { + maxCodeLength := uint32(0) + for _, cl := range codeLengths { + if maxCodeLength < cl { + maxCodeLength = cl + } + } + const maxAllowedCodeLength = 15 + if len(codeLengths) == 0 || maxCodeLength > maxAllowedCodeLength { + return nil, errInvalidHuffmanTree + } + histogram := [maxAllowedCodeLength + 1]uint32{} + for _, cl := range codeLengths { + histogram[cl]++ + } + currCode, nextCodes := uint32(0), [maxAllowedCodeLength + 1]uint32{} + for cl := 1; cl < len(nextCodes); cl++ { + currCode = (currCode + histogram[cl-1]) << 1 + nextCodes[cl] = currCode + } + codes := make([]uint32, len(codeLengths)) + for symbol, cl := range codeLengths { + if cl > 0 { + codes[symbol] = nextCodes[cl] + nextCodes[cl]++ + } + } + return codes, nil +} + +// build builds a canonical Huffman tree from the given code lengths. +func (h *hTree) build(codeLengths []uint32) error { + // Calculate the number of symbols. + var nSymbols, lastSymbol uint32 + for symbol, cl := range codeLengths { + if cl != 0 { + nSymbols++ + lastSymbol = uint32(symbol) + } + } + if nSymbols == 0 { + return errInvalidHuffmanTree + } + h.nodes = make([]hNode, 1, 2*nSymbols-1) + // Handle the trivial case. + if nSymbols == 1 { + if len(codeLengths) <= int(lastSymbol) { + return errInvalidHuffmanTree + } + return h.insert(lastSymbol, 0, 0) + } + // Handle the non-trivial case. + codes, err := codeLengthsToCodes(codeLengths) + if err != nil { + return err + } + for symbol, cl := range codeLengths { + if cl > 0 { + if err := h.insert(uint32(symbol), codes[symbol], cl); err != nil { + return err + } + } + } + return nil +} + +// buildSimple builds a Huffman tree with 1 or 2 symbols. +func (h *hTree) buildSimple(nSymbols uint32, symbols [2]uint32, alphabetSize uint32) error { + h.nodes = make([]hNode, 1, 2*nSymbols-1) + for i := uint32(0); i < nSymbols; i++ { + if symbols[i] >= alphabetSize { + return errInvalidHuffmanTree + } + if err := h.insert(symbols[i], i, nSymbols-1); err != nil { + return err + } + } + return nil +} + +// next returns the next Huffman-encoded symbol from the bit-stream d. +func (h *hTree) next(d *decoder) (uint32, error) { + var n uint32 + // Read enough bits so that we can use the look-up table. + if d.nBits < lutSize { + c, err := d.r.ReadByte() + if err != nil { + if err == io.EOF { + // There are no more bytes of data, but we may still be able + // to read the next symbol out of the previously read bits. + goto slowPath + } + return 0, err + } + d.bits |= uint32(c) << d.nBits + d.nBits += 8 + } + // Use the look-up table. + n = h.lut[d.bits&lutMask] + if b := n & 0xff; b != 0 { + b-- + d.bits >>= b + d.nBits -= b + return n >> 8, nil + } + n >>= 8 + d.bits >>= lutSize + d.nBits -= lutSize + +slowPath: + for h.nodes[n].children != leafNode { + if d.nBits == 0 { + c, err := d.r.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return 0, err + } + d.bits = uint32(c) + d.nBits = 8 + } + n = uint32(h.nodes[n].children) + 1&d.bits + d.bits >>= 1 + d.nBits-- + } + return h.nodes[n].symbol, nil +} diff --git a/vendor/golang.org/x/image/vp8l/transform.go b/vendor/golang.org/x/image/vp8l/transform.go new file mode 100644 index 00000000..06543dac --- /dev/null +++ b/vendor/golang.org/x/image/vp8l/transform.go @@ -0,0 +1,299 @@ +// Copyright 2014 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. + +package vp8l + +// This file deals with image transforms, specified in section 3. + +// nTiles returns the number of tiles needed to cover size pixels, where each +// tile's side is 1<> bits +} + +const ( + transformTypePredictor = 0 + transformTypeCrossColor = 1 + transformTypeSubtractGreen = 2 + transformTypeColorIndexing = 3 + nTransformTypes = 4 +) + +// transform holds the parameters for an invertible transform. +type transform struct { + // transformType is the type of the transform. + transformType uint32 + // oldWidth is the width of the image before transformation (or + // equivalently, after inverse transformation). The color-indexing + // transform can reduce the width. For example, a 50-pixel-wide + // image that only needs 4 bits (half a byte) per color index can + // be transformed into a 25-pixel-wide image. + oldWidth int32 + // bits is the log-2 size of the transform's tiles, for the predictor + // and cross-color transforms. 8>>bits is the number of bits per + // color index, for the color-index transform. + bits uint32 + // pix is the tile values, for the predictor and cross-color + // transforms, and the color palette, for the color-index transform. + pix []byte +} + +var inverseTransforms = [nTransformTypes]func(*transform, []byte, int32) []byte{ + transformTypePredictor: inversePredictor, + transformTypeCrossColor: inverseCrossColor, + transformTypeSubtractGreen: inverseSubtractGreen, + transformTypeColorIndexing: inverseColorIndexing, +} + +func inversePredictor(t *transform, pix []byte, h int32) []byte { + if t.oldWidth == 0 || h == 0 { + return pix + } + // The first pixel's predictor is mode 0 (opaque black). + pix[3] += 0xff + p, mask := int32(4), int32(1)<> t.bits) * tilesPerRow + predictorMode := t.pix[q+1] & 0x0f + q += 4 + for x := int32(1); x < t.oldWidth; x++ { + if x&mask == 0 { + predictorMode = t.pix[q+1] & 0x0f + q += 4 + } + switch predictorMode { + case 0: // Opaque black. + pix[p+3] += 0xff + + case 1: // L. + pix[p+0] += pix[p-4] + pix[p+1] += pix[p-3] + pix[p+2] += pix[p-2] + pix[p+3] += pix[p-1] + + case 2: // T. + pix[p+0] += pix[top+0] + pix[p+1] += pix[top+1] + pix[p+2] += pix[top+2] + pix[p+3] += pix[top+3] + + case 3: // TR. + pix[p+0] += pix[top+4] + pix[p+1] += pix[top+5] + pix[p+2] += pix[top+6] + pix[p+3] += pix[top+7] + + case 4: // TL. + pix[p+0] += pix[top-4] + pix[p+1] += pix[top-3] + pix[p+2] += pix[top-2] + pix[p+3] += pix[top-1] + + case 5: // Average2(Average2(L, TR), T). + pix[p+0] += avg2(avg2(pix[p-4], pix[top+4]), pix[top+0]) + pix[p+1] += avg2(avg2(pix[p-3], pix[top+5]), pix[top+1]) + pix[p+2] += avg2(avg2(pix[p-2], pix[top+6]), pix[top+2]) + pix[p+3] += avg2(avg2(pix[p-1], pix[top+7]), pix[top+3]) + + case 6: // Average2(L, TL). + pix[p+0] += avg2(pix[p-4], pix[top-4]) + pix[p+1] += avg2(pix[p-3], pix[top-3]) + pix[p+2] += avg2(pix[p-2], pix[top-2]) + pix[p+3] += avg2(pix[p-1], pix[top-1]) + + case 7: // Average2(L, T). + pix[p+0] += avg2(pix[p-4], pix[top+0]) + pix[p+1] += avg2(pix[p-3], pix[top+1]) + pix[p+2] += avg2(pix[p-2], pix[top+2]) + pix[p+3] += avg2(pix[p-1], pix[top+3]) + + case 8: // Average2(TL, T). + pix[p+0] += avg2(pix[top-4], pix[top+0]) + pix[p+1] += avg2(pix[top-3], pix[top+1]) + pix[p+2] += avg2(pix[top-2], pix[top+2]) + pix[p+3] += avg2(pix[top-1], pix[top+3]) + + case 9: // Average2(T, TR). + pix[p+0] += avg2(pix[top+0], pix[top+4]) + pix[p+1] += avg2(pix[top+1], pix[top+5]) + pix[p+2] += avg2(pix[top+2], pix[top+6]) + pix[p+3] += avg2(pix[top+3], pix[top+7]) + + case 10: // Average2(Average2(L, TL), Average2(T, TR)). + pix[p+0] += avg2(avg2(pix[p-4], pix[top-4]), avg2(pix[top+0], pix[top+4])) + pix[p+1] += avg2(avg2(pix[p-3], pix[top-3]), avg2(pix[top+1], pix[top+5])) + pix[p+2] += avg2(avg2(pix[p-2], pix[top-2]), avg2(pix[top+2], pix[top+6])) + pix[p+3] += avg2(avg2(pix[p-1], pix[top-1]), avg2(pix[top+3], pix[top+7])) + + case 11: // Select(L, T, TL). + l0 := int32(pix[p-4]) + l1 := int32(pix[p-3]) + l2 := int32(pix[p-2]) + l3 := int32(pix[p-1]) + c0 := int32(pix[top-4]) + c1 := int32(pix[top-3]) + c2 := int32(pix[top-2]) + c3 := int32(pix[top-1]) + t0 := int32(pix[top+0]) + t1 := int32(pix[top+1]) + t2 := int32(pix[top+2]) + t3 := int32(pix[top+3]) + l := abs(c0-t0) + abs(c1-t1) + abs(c2-t2) + abs(c3-t3) + t := abs(c0-l0) + abs(c1-l1) + abs(c2-l2) + abs(c3-l3) + if l < t { + pix[p+0] += uint8(l0) + pix[p+1] += uint8(l1) + pix[p+2] += uint8(l2) + pix[p+3] += uint8(l3) + } else { + pix[p+0] += uint8(t0) + pix[p+1] += uint8(t1) + pix[p+2] += uint8(t2) + pix[p+3] += uint8(t3) + } + + case 12: // ClampAddSubtractFull(L, T, TL). + pix[p+0] += clampAddSubtractFull(pix[p-4], pix[top+0], pix[top-4]) + pix[p+1] += clampAddSubtractFull(pix[p-3], pix[top+1], pix[top-3]) + pix[p+2] += clampAddSubtractFull(pix[p-2], pix[top+2], pix[top-2]) + pix[p+3] += clampAddSubtractFull(pix[p-1], pix[top+3], pix[top-1]) + + case 13: // ClampAddSubtractHalf(Average2(L, T), TL). + pix[p+0] += clampAddSubtractHalf(avg2(pix[p-4], pix[top+0]), pix[top-4]) + pix[p+1] += clampAddSubtractHalf(avg2(pix[p-3], pix[top+1]), pix[top-3]) + pix[p+2] += clampAddSubtractHalf(avg2(pix[p-2], pix[top+2]), pix[top-2]) + pix[p+3] += clampAddSubtractHalf(avg2(pix[p-1], pix[top+3]), pix[top-1]) + } + p, top = p+4, top+4 + } + } + return pix +} + +func inverseCrossColor(t *transform, pix []byte, h int32) []byte { + var greenToRed, greenToBlue, redToBlue int32 + p, mask, tilesPerRow := int32(0), int32(1)<> t.bits) * tilesPerRow + for x := int32(0); x < t.oldWidth; x++ { + if x&mask == 0 { + redToBlue = int32(int8(t.pix[q+0])) + greenToBlue = int32(int8(t.pix[q+1])) + greenToRed = int32(int8(t.pix[q+2])) + q += 4 + } + red := pix[p+0] + green := pix[p+1] + blue := pix[p+2] + red += uint8(uint32(greenToRed*int32(int8(green))) >> 5) + blue += uint8(uint32(greenToBlue*int32(int8(green))) >> 5) + blue += uint8(uint32(redToBlue*int32(int8(red))) >> 5) + pix[p+0] = red + pix[p+2] = blue + p += 4 + } + } + return pix +} + +func inverseSubtractGreen(t *transform, pix []byte, h int32) []byte { + for p := 0; p < len(pix); p += 4 { + green := pix[p+1] + pix[p+0] += green + pix[p+2] += green + } + return pix +} + +func inverseColorIndexing(t *transform, pix []byte, h int32) []byte { + if t.bits == 0 { + for p := 0; p < len(pix); p += 4 { + i := 4 * uint32(pix[p+1]) + pix[p+0] = t.pix[i+0] + pix[p+1] = t.pix[i+1] + pix[p+2] = t.pix[i+2] + pix[p+3] = t.pix[i+3] + } + return pix + } + + vMask, xMask, bitsPerPixel := uint32(0), int32(0), uint32(8>>t.bits) + switch t.bits { + case 1: + vMask, xMask = 0x0f, 0x01 + case 2: + vMask, xMask = 0x03, 0x03 + case 3: + vMask, xMask = 0x01, 0x07 + } + + d, p, v, dst := 0, 0, uint32(0), make([]byte, 4*t.oldWidth*h) + for y := int32(0); y < h; y++ { + for x := int32(0); x < t.oldWidth; x++ { + if x&xMask == 0 { + v = uint32(pix[p+1]) + p += 4 + } + + i := 4 * (v & vMask) + dst[d+0] = t.pix[i+0] + dst[d+1] = t.pix[i+1] + dst[d+2] = t.pix[i+2] + dst[d+3] = t.pix[i+3] + d += 4 + + v >>= bitsPerPixel + } + } + return dst +} + +func abs(x int32) int32 { + if x < 0 { + return -x + } + return x +} + +func avg2(a, b uint8) uint8 { + return uint8((int32(a) + int32(b)) / 2) +} + +func clampAddSubtractFull(a, b, c uint8) uint8 { + x := int32(a) + int32(b) - int32(c) + if x < 0 { + return 0 + } + if x > 255 { + return 255 + } + return uint8(x) +} + +func clampAddSubtractHalf(a, b uint8) uint8 { + x := int32(a) + (int32(a)-int32(b))/2 + if x < 0 { + return 0 + } + if x > 255 { + return 255 + } + return uint8(x) +} diff --git a/vendor/golang.org/x/image/webp/decode.go b/vendor/golang.org/x/image/webp/decode.go new file mode 100644 index 00000000..e211c7d5 --- /dev/null +++ b/vendor/golang.org/x/image/webp/decode.go @@ -0,0 +1,276 @@ +// Copyright 2011 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. + +package webp + +import ( + "bytes" + "errors" + "image" + "image/color" + "io" + + "golang.org/x/image/riff" + "golang.org/x/image/vp8" + "golang.org/x/image/vp8l" +) + +var errInvalidFormat = errors.New("webp: invalid format") + +var ( + fccALPH = riff.FourCC{'A', 'L', 'P', 'H'} + fccVP8 = riff.FourCC{'V', 'P', '8', ' '} + fccVP8L = riff.FourCC{'V', 'P', '8', 'L'} + fccVP8X = riff.FourCC{'V', 'P', '8', 'X'} + fccWEBP = riff.FourCC{'W', 'E', 'B', 'P'} +) + +func decode(r io.Reader, configOnly bool) (image.Image, image.Config, error) { + formType, riffReader, err := riff.NewReader(r) + if err != nil { + return nil, image.Config{}, err + } + if formType != fccWEBP { + return nil, image.Config{}, errInvalidFormat + } + + var ( + alpha []byte + alphaStride int + wantAlpha bool + seenVP8X bool + widthMinusOne uint32 + heightMinusOne uint32 + buf [10]byte + ) + for { + chunkID, chunkLen, chunkData, err := riffReader.Next() + if err == io.EOF { + err = errInvalidFormat + } + if err != nil { + return nil, image.Config{}, err + } + + switch chunkID { + case fccALPH: + if !wantAlpha { + return nil, image.Config{}, errInvalidFormat + } + wantAlpha = false + // Read the Pre-processing | Filter | Compression byte. + if _, err := io.ReadFull(chunkData, buf[:1]); err != nil { + if err == io.EOF { + err = errInvalidFormat + } + return nil, image.Config{}, err + } + alpha, alphaStride, err = readAlpha(chunkData, widthMinusOne, heightMinusOne, buf[0]&0x03) + if err != nil { + return nil, image.Config{}, err + } + unfilterAlpha(alpha, alphaStride, (buf[0]>>2)&0x03) + + case fccVP8: + if wantAlpha || int32(chunkLen) < 0 { + return nil, image.Config{}, errInvalidFormat + } + d := vp8.NewDecoder() + d.Init(chunkData, int(chunkLen)) + fh, err := d.DecodeFrameHeader() + if err != nil { + return nil, image.Config{}, err + } + if configOnly { + return nil, image.Config{ + ColorModel: color.YCbCrModel, + Width: fh.Width, + Height: fh.Height, + }, nil + } + m, err := d.DecodeFrame() + if err != nil { + return nil, image.Config{}, err + } + if alpha != nil { + return &image.NYCbCrA{ + YCbCr: *m, + A: alpha, + AStride: alphaStride, + }, image.Config{}, nil + } + return m, image.Config{}, nil + + case fccVP8L: + if wantAlpha || alpha != nil { + return nil, image.Config{}, errInvalidFormat + } + if configOnly { + c, err := vp8l.DecodeConfig(chunkData) + return nil, c, err + } + m, err := vp8l.Decode(chunkData) + return m, image.Config{}, err + + case fccVP8X: + if seenVP8X { + return nil, image.Config{}, errInvalidFormat + } + seenVP8X = true + if chunkLen != 10 { + return nil, image.Config{}, errInvalidFormat + } + if _, err := io.ReadFull(chunkData, buf[:10]); err != nil { + return nil, image.Config{}, err + } + const ( + animationBit = 1 << 1 + xmpMetadataBit = 1 << 2 + exifMetadataBit = 1 << 3 + alphaBit = 1 << 4 + iccProfileBit = 1 << 5 + ) + wantAlpha = (buf[0] & alphaBit) != 0 + widthMinusOne = uint32(buf[4]) | uint32(buf[5])<<8 | uint32(buf[6])<<16 + heightMinusOne = uint32(buf[7]) | uint32(buf[8])<<8 | uint32(buf[9])<<16 + if configOnly { + if wantAlpha { + return nil, image.Config{ + ColorModel: color.NYCbCrAModel, + Width: int(widthMinusOne) + 1, + Height: int(heightMinusOne) + 1, + }, nil + } + return nil, image.Config{ + ColorModel: color.YCbCrModel, + Width: int(widthMinusOne) + 1, + Height: int(heightMinusOne) + 1, + }, nil + } + } + } +} + +func readAlpha(chunkData io.Reader, widthMinusOne, heightMinusOne uint32, compression byte) ( + alpha []byte, alphaStride int, err error) { + + switch compression { + case 0: + w := int(widthMinusOne) + 1 + h := int(heightMinusOne) + 1 + alpha = make([]byte, w*h) + if _, err := io.ReadFull(chunkData, alpha); err != nil { + return nil, 0, err + } + return alpha, w, nil + + case 1: + // Read the VP8L-compressed alpha values. First, synthesize a 5-byte VP8L header: + // a 1-byte magic number, a 14-bit widthMinusOne, a 14-bit heightMinusOne, + // a 1-bit (ignored, zero) alphaIsUsed and a 3-bit (zero) version. + // TODO(nigeltao): be more efficient than decoding an *image.NRGBA just to + // extract the green values to a separately allocated []byte. Fixing this + // will require changes to the vp8l package's API. + if widthMinusOne > 0x3fff || heightMinusOne > 0x3fff { + return nil, 0, errors.New("webp: invalid format") + } + alphaImage, err := vp8l.Decode(io.MultiReader( + bytes.NewReader([]byte{ + 0x2f, // VP8L magic number. + uint8(widthMinusOne), + uint8(widthMinusOne>>8) | uint8(heightMinusOne<<6), + uint8(heightMinusOne >> 2), + uint8(heightMinusOne >> 10), + }), + chunkData, + )) + if err != nil { + return nil, 0, err + } + // The green values of the inner NRGBA image are the alpha values of the + // outer NYCbCrA image. + pix := alphaImage.(*image.NRGBA).Pix + alpha = make([]byte, len(pix)/4) + for i := range alpha { + alpha[i] = pix[4*i+1] + } + return alpha, int(widthMinusOne) + 1, nil + } + return nil, 0, errInvalidFormat +} + +func unfilterAlpha(alpha []byte, alphaStride int, filter byte) { + if len(alpha) == 0 || alphaStride == 0 { + return + } + switch filter { + case 1: // Horizontal filter. + for i := 1; i < alphaStride; i++ { + alpha[i] += alpha[i-1] + } + for i := alphaStride; i < len(alpha); i += alphaStride { + // The first column is equivalent to the vertical filter. + alpha[i] += alpha[i-alphaStride] + + for j := 1; j < alphaStride; j++ { + alpha[i+j] += alpha[i+j-1] + } + } + + case 2: // Vertical filter. + // The first row is equivalent to the horizontal filter. + for i := 1; i < alphaStride; i++ { + alpha[i] += alpha[i-1] + } + + for i := alphaStride; i < len(alpha); i++ { + alpha[i] += alpha[i-alphaStride] + } + + case 3: // Gradient filter. + // The first row is equivalent to the horizontal filter. + for i := 1; i < alphaStride; i++ { + alpha[i] += alpha[i-1] + } + + for i := alphaStride; i < len(alpha); i += alphaStride { + // The first column is equivalent to the vertical filter. + alpha[i] += alpha[i-alphaStride] + + // The interior is predicted on the three top/left pixels. + for j := 1; j < alphaStride; j++ { + c := int(alpha[i+j-alphaStride-1]) + b := int(alpha[i+j-alphaStride]) + a := int(alpha[i+j-1]) + x := a + b - c + if x < 0 { + x = 0 + } else if x > 255 { + x = 255 + } + alpha[i+j] += uint8(x) + } + } + } +} + +// Decode reads a WEBP image from r and returns it as an image.Image. +func Decode(r io.Reader) (image.Image, error) { + m, _, err := decode(r, false) + if err != nil { + return nil, err + } + return m, err +} + +// DecodeConfig returns the color model and dimensions of a WEBP image without +// decoding the entire image. +func DecodeConfig(r io.Reader) (image.Config, error) { + _, c, err := decode(r, true) + return c, err +} + +func init() { + image.RegisterFormat("webp", "RIFF????WEBPVP8", Decode, DecodeConfig) +} diff --git a/vendor/golang.org/x/image/webp/doc.go b/vendor/golang.org/x/image/webp/doc.go new file mode 100644 index 00000000..e321c854 --- /dev/null +++ b/vendor/golang.org/x/image/webp/doc.go @@ -0,0 +1,9 @@ +// Copyright 2016 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. + +// Package webp implements a decoder for WEBP images. +// +// WEBP is defined at: +// https://developers.google.com/speed/webp/docs/riff_container +package webp // import "golang.org/x/image/webp" diff --git a/vendor/modules.txt b/vendor/modules.txt index 69e749f5..b075ba8b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -292,6 +292,12 @@ github.com/hashicorp/errwrap # github.com/hashicorp/go-multierror v1.1.1 ## explicit; go 1.13 github.com/hashicorp/go-multierror +# github.com/hhrutter/lzw v1.0.0 +## explicit; go 1.19 +github.com/hhrutter/lzw +# github.com/hhrutter/tiff v1.0.1 +## explicit; go 1.19 +github.com/hhrutter/tiff # github.com/jackc/pgpassfile v1.0.0 ## explicit; go 1.12 github.com/jackc/pgpassfile @@ -417,6 +423,22 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/pashagolub/pgxmock/v3 v3.4.0 ## explicit; go 1.21 github.com/pashagolub/pgxmock/v3 +# github.com/pdfcpu/pdfcpu v0.9.1 +## explicit; go 1.22 +github.com/pdfcpu/pdfcpu/internal/corefont/metrics +github.com/pdfcpu/pdfcpu/pkg/filter +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/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/primitives +github.com/pdfcpu/pdfcpu/pkg/pdfcpu/scan +github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types # github.com/perimeterx/marshmallow v1.1.5 ## explicit; go 1.17 github.com/perimeterx/marshmallow @@ -590,6 +612,13 @@ golang.org/x/crypto/pbkdf2 golang.org/x/crypto/sha3 golang.org/x/crypto/ssh golang.org/x/crypto/ssh/internal/bcrypt_pbkdf +# golang.org/x/image v0.21.0 +## explicit; go 1.18 +golang.org/x/image/ccitt +golang.org/x/image/riff +golang.org/x/image/vp8 +golang.org/x/image/vp8l +golang.org/x/image/webp # golang.org/x/net v0.34.0 ## explicit; go 1.18 golang.org/x/net/html