From aefa4de0dc4299866b79f849282a2960bfdae109 Mon Sep 17 00:00:00 2001 From: Sacha Bron Date: Tue, 6 Aug 2019 14:36:54 +0200 Subject: [PATCH] First public release :rocket: --- .dockerignore | 1 + .drone.yml | 159 + .gitignore | 108 + .prettierrc.js | 8 + .s2i/bin/assemble | 20 + .s2i/bin/run | 3 + .vscode/launch.json | 28 + .vscode/tasks.json | 16 + CONTRIBUTING.md | 61 + LICENSE | 201 + README.md | 331 ++ api/server/package-lock.json | 832 +++ api/server/package.json | 29 + api/server/src/FileManager.ts | 112 + api/server/src/Logger.ts | 25 + api/server/src/ProcessManager.ts | 86 + api/server/src/api.ts | 292 + api/server/src/index.ts | 21 + api/server/src/types.ts | 76 + api/server/tsconfig.json | 14 + api/swagger/openapi.yaml | 436 ++ api/swagger/swagger-generated.yaml | 518 ++ .../Jupyter - API Access Demo.ipynb | 232 + demo/jupyter-notebook/parsr_api.py | 81 + .../parsr_output_interpreter.py | 85 + demo/jupyter-notebook/sampleConfig.json | 44 + demo/jupyter-notebook/sampleFile.pdf | Bin 0 -> 44003 bytes demo/python-module/README.md | 22 + demo/python-module/echo-module.py | 51 + demo/web-viewer/.gitignore | 5 + demo/web-viewer/index.js | 238 + demo/web-viewer/localStorage.js | 57 + demo/web-viewer/package-lock.json | 2448 ++++++++ demo/web-viewer/package.json | 24 + demo/web-viewer/public/css/style.css | 33 + demo/web-viewer/public/css/viewer.css | 105 + demo/web-viewer/public/favicon.ico | Bin 0 -> 1150 bytes demo/web-viewer/public/index.html | 81 + demo/web-viewer/public/js/renderer.js | 346 ++ demo/web-viewer/public/js/viewer.js | 62 + demo/web-viewer/public/viewer.html | 54 + demo/web-viewer/public/views/downloader.tpl | 30 + demo/web-viewer/public/views/loader.tpl | 38 + .../web-viewer/public/views/visualization.tpl | 47 + demo/web-viewer/style/style.scss | 164 + docker-compose.yml | 32 + docker/duckling/Dockerfile | 24 + docker/parsr/Dockerfile | 77 + docs/API-deprecated.md | 102 + docs/api-guide.md | 216 + docs/api.html | 5076 +++++++++++++++++ docs/architecture.md | 0 docs/configuration-file.md | 170 + docs/create-your-module.md | 28 + docs/docker.md | 23 + docs/json-output.md | 373 ++ .../modules/header-footer-detection-module.md | 37 + docs/modules/heading-detection-module.md | 29 + docs/modules/hierarchy-detection-module.md | 15 + docs/modules/key-value-detection-module.md | 53 + docs/modules/lines-to-paragraph-module.md | 27 + docs/modules/link-detection-module.md | 27 + docs/modules/list-detection-module.md | 27 + docs/modules/number-correction-module.md | 33 + docs/modules/out-of-page-removal-module.md | 26 + .../modules/reading-order-detection-module.md | 29 + docs/modules/redundancy-detection-module.md | 29 + docs/modules/remote-module.md | 18 + docs/modules/whitespace-removal-module.md | 25 + docs/modules/words-to-line-module.md | 26 + package-lock.json | 4471 +++++++++++++++ package.json | 93 + server/bin/index.ts | 298 + server/configKeyValueSearch.json | 107 + server/defaultConfig.json | 44 + server/remoteModuleConfig.json | 46 + server/src/Cleaner.ts | 211 + server/src/Orchestrator.ts | 59 + server/src/exporters/ConfidencesExporter.ts | 23 + server/src/exporters/CsvExporter.ts | 60 + server/src/exporters/Exporter.ts | 43 + server/src/exporters/JsonCompactExporter.ts | 23 + server/src/exporters/JsonExporter.ts | 273 + server/src/exporters/MarkdownExporter.ts | 83 + server/src/exporters/PdfExporter.ts | 70 + server/src/exporters/TextExporter.ts | 66 + server/src/exporters/XmlExporter.ts | 23 + server/src/exporters/index.ts | 25 + server/src/extractors/Extractor.ts | 34 + server/src/extractors/abbyy/AbbyyClient.ts | 362 ++ server/src/extractors/abbyy/AbbyyTools.ts | 456 ++ server/src/extractors/abbyy/AbbyyToolsXml.ts | 51 + server/src/extractors/extract-fonts.ts | 58 + server/src/extractors/json/JsonExtractor.ts | 30 + .../extractors/pdf2json/PdfJsonExtractor.ts | 36 + server/src/extractors/pdf2json/pdf2json.ts | 164 + server/src/extractors/set-page-dimensions.ts | 62 + .../tesseract/TesseractExtractor.ts | 36 + .../extractors/tesseract/tesseract2json.ts | 144 + .../modules/HeaderFooterDetectionModule.ts | 205 + server/src/modules/HeadingDetectionModule.ts | 153 + .../src/modules/HierarchyDetectionModule.ts | 143 + server/src/modules/KeyValueDetectionModule.ts | 238 + server/src/modules/LinesToParagraphModule.ts | 134 + server/src/modules/LinkDetectionModule.ts | 61 + server/src/modules/ListDetectionModule.ts | 48 + server/src/modules/Module.ts | 57 + server/src/modules/NumberCorrectionModule.ts | 270 + server/src/modules/OutOfPageRemovalModule.ts | 38 + .../modules/ReadingOrderDetectionModule.ts | 204 + .../src/modules/RedundancyDetectionModule.ts | 120 + server/src/modules/RegexMatcherModule.ts | 84 + server/src/modules/RemoteModule.ts | 53 + server/src/modules/SeparateWordsModule.ts | 197 + server/src/modules/TemplateModule.ts | 69 + server/src/modules/WhitespaceRemovalModule.ts | 70 + server/src/modules/WordsToLineModule.ts | 117 + server/src/tslint.conf | 47 + server/src/types/Config.ts | 228 + .../types/DocumentRepresentation/Barcode.ts | 65 + .../DocumentRepresentation/BoundingBox.ts | 160 + .../types/DocumentRepresentation/Character.ts | 54 + .../src/types/DocumentRepresentation/Color.ts | 20 + .../types/DocumentRepresentation/Document.ts | 97 + .../types/DocumentRepresentation/Drawing.ts | 48 + .../types/DocumentRepresentation/Element.ts | 215 + .../src/types/DocumentRepresentation/Font.ts | 222 + .../types/DocumentRepresentation/Heading.ts | 48 + .../src/types/DocumentRepresentation/Image.ts | 48 + .../DocumentRepresentation/JsonExport.ts | 82 + .../src/types/DocumentRepresentation/Line.ts | 90 + .../src/types/DocumentRepresentation/List.ts | 89 + .../src/types/DocumentRepresentation/Page.ts | 281 + .../types/DocumentRepresentation/Paragraph.ts | 162 + .../types/DocumentRepresentation/SvgLine.ts | 139 + .../types/DocumentRepresentation/SvgShape.ts | 19 + .../src/types/DocumentRepresentation/Table.ts | 327 ++ .../types/DocumentRepresentation/TableCell.ts | 89 + .../types/DocumentRepresentation/TableRow.ts | 44 + .../src/types/DocumentRepresentation/Text.ts | 58 + .../src/types/DocumentRepresentation/Word.ts | 111 + .../src/types/DocumentRepresentation/index.ts | 35 + server/src/types/Metadata/ComplexMetadata.ts | 60 + server/src/types/Metadata/KeyValueMetadata.ts | 26 + server/src/types/Metadata/Metadata.ts | 21 + server/src/types/Metadata/NumberMetadata.ts | 60 + server/src/types/Metadata/Properties.ts | 33 + server/src/types/Metadata/RegexMetadata.ts | 26 + server/src/types/Metadata/index.ts | 22 + server/src/types/Pdf2JsonFont.ts | 22 + server/src/types/Pdf2JsonPage.ts | 36 + server/src/types/Pdf2JsonText.ts | 24 + server/src/types/TableInfo.ts | 38 + server/src/types/TableReconstruction.ts | 23 + server/src/types/TsvElement.ts | 32 + server/src/utils.ts | 590 ++ server/src/utils/Logger.ts | 95 + server/src/utils/json2document.ts | 402 ++ sonar-project.properties | 3 + test/assets/html.pdf | Bin 0 -> 21985 bytes test/assets/line-merge-2.pdf | Bin 0 -> 7602 bytes test/assets/line-merge-2.pdf.json | 19 + test/assets/line-merge.pdf | Bin 0 -> 18847 bytes test/assets/line-merge.pdf.json | 23 + test/assets/lists.pdf | Bin 0 -> 20736 bytes test/assets/number-correction-1.pdf | Bin 0 -> 15866 bytes test/assets/number-correction-1.pdf.json | 322 ++ test/assets/page-number.pdf | Bin 0 -> 15350 bytes test/assets/page-numbers.pdf | Bin 0 -> 20598 bytes test/assets/paragraph-merge-1.pdf | Bin 0 -> 10631 bytes test/assets/paragraph-merge-1.pdf.json | 39 + test/assets/paragraph-merge.pdf | Bin 0 -> 27816 bytes test/assets/paragraph-merge.pdf.json | 362 ++ test/assets/redundancy-detection.pdf | Bin 0 -> 13856 bytes test/assets/text-order-detection.pdf | Bin 0 -> 61990 bytes test/assets/text-order-detection.pdf.json | 1309 +++++ test/assets/text-order-mini.pdf | Bin 0 -> 17804 bytes test/helpers.ts | 56 + test/json-export-import.spec.ts | 83 + test/line-merge.spec.ts | 63 + test/number-correction.spec.ts | 151 + test/paragraph-merge.spec.ts | 65 + test/redundancy-detection.spec.ts | 45 + test/text-order-detection.spec.ts | 65 + test/utils.spec.ts | 130 + tsconfig.json | 19 + tslint.json | 18 + 187 files changed, 30918 insertions(+) create mode 100644 .dockerignore create mode 100644 .drone.yml create mode 100644 .gitignore create mode 100644 .prettierrc.js create mode 100755 .s2i/bin/assemble create mode 100755 .s2i/bin/run create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 api/server/package-lock.json create mode 100644 api/server/package.json create mode 100644 api/server/src/FileManager.ts create mode 100644 api/server/src/Logger.ts create mode 100644 api/server/src/ProcessManager.ts create mode 100644 api/server/src/api.ts create mode 100644 api/server/src/index.ts create mode 100644 api/server/src/types.ts create mode 100644 api/server/tsconfig.json create mode 100644 api/swagger/openapi.yaml create mode 100755 api/swagger/swagger-generated.yaml create mode 100644 demo/jupyter-notebook/Jupyter - API Access Demo.ipynb create mode 100644 demo/jupyter-notebook/parsr_api.py create mode 100644 demo/jupyter-notebook/parsr_output_interpreter.py create mode 100644 demo/jupyter-notebook/sampleConfig.json create mode 100644 demo/jupyter-notebook/sampleFile.pdf create mode 100644 demo/python-module/README.md create mode 100644 demo/python-module/echo-module.py create mode 100644 demo/web-viewer/.gitignore create mode 100644 demo/web-viewer/index.js create mode 100644 demo/web-viewer/localStorage.js create mode 100644 demo/web-viewer/package-lock.json create mode 100644 demo/web-viewer/package.json create mode 100644 demo/web-viewer/public/css/style.css create mode 100644 demo/web-viewer/public/css/viewer.css create mode 100644 demo/web-viewer/public/favicon.ico create mode 100644 demo/web-viewer/public/index.html create mode 100644 demo/web-viewer/public/js/renderer.js create mode 100644 demo/web-viewer/public/js/viewer.js create mode 100644 demo/web-viewer/public/viewer.html create mode 100644 demo/web-viewer/public/views/downloader.tpl create mode 100644 demo/web-viewer/public/views/loader.tpl create mode 100644 demo/web-viewer/public/views/visualization.tpl create mode 100644 demo/web-viewer/style/style.scss create mode 100644 docker-compose.yml create mode 100644 docker/duckling/Dockerfile create mode 100644 docker/parsr/Dockerfile create mode 100644 docs/API-deprecated.md create mode 100644 docs/api-guide.md create mode 100755 docs/api.html create mode 100644 docs/architecture.md create mode 100644 docs/configuration-file.md create mode 100644 docs/create-your-module.md create mode 100644 docs/docker.md create mode 100644 docs/json-output.md create mode 100644 docs/modules/header-footer-detection-module.md create mode 100644 docs/modules/heading-detection-module.md create mode 100644 docs/modules/hierarchy-detection-module.md create mode 100644 docs/modules/key-value-detection-module.md create mode 100644 docs/modules/lines-to-paragraph-module.md create mode 100644 docs/modules/link-detection-module.md create mode 100644 docs/modules/list-detection-module.md create mode 100644 docs/modules/number-correction-module.md create mode 100644 docs/modules/out-of-page-removal-module.md create mode 100644 docs/modules/reading-order-detection-module.md create mode 100644 docs/modules/redundancy-detection-module.md create mode 100644 docs/modules/remote-module.md create mode 100644 docs/modules/whitespace-removal-module.md create mode 100644 docs/modules/words-to-line-module.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/bin/index.ts create mode 100644 server/configKeyValueSearch.json create mode 100644 server/defaultConfig.json create mode 100644 server/remoteModuleConfig.json create mode 100644 server/src/Cleaner.ts create mode 100644 server/src/Orchestrator.ts create mode 100644 server/src/exporters/ConfidencesExporter.ts create mode 100644 server/src/exporters/CsvExporter.ts create mode 100644 server/src/exporters/Exporter.ts create mode 100644 server/src/exporters/JsonCompactExporter.ts create mode 100644 server/src/exporters/JsonExporter.ts create mode 100644 server/src/exporters/MarkdownExporter.ts create mode 100644 server/src/exporters/PdfExporter.ts create mode 100644 server/src/exporters/TextExporter.ts create mode 100644 server/src/exporters/XmlExporter.ts create mode 100644 server/src/exporters/index.ts create mode 100644 server/src/extractors/Extractor.ts create mode 100644 server/src/extractors/abbyy/AbbyyClient.ts create mode 100644 server/src/extractors/abbyy/AbbyyTools.ts create mode 100644 server/src/extractors/abbyy/AbbyyToolsXml.ts create mode 100644 server/src/extractors/extract-fonts.ts create mode 100644 server/src/extractors/json/JsonExtractor.ts create mode 100644 server/src/extractors/pdf2json/PdfJsonExtractor.ts create mode 100644 server/src/extractors/pdf2json/pdf2json.ts create mode 100644 server/src/extractors/set-page-dimensions.ts create mode 100644 server/src/extractors/tesseract/TesseractExtractor.ts create mode 100644 server/src/extractors/tesseract/tesseract2json.ts create mode 100644 server/src/modules/HeaderFooterDetectionModule.ts create mode 100644 server/src/modules/HeadingDetectionModule.ts create mode 100644 server/src/modules/HierarchyDetectionModule.ts create mode 100644 server/src/modules/KeyValueDetectionModule.ts create mode 100644 server/src/modules/LinesToParagraphModule.ts create mode 100644 server/src/modules/LinkDetectionModule.ts create mode 100644 server/src/modules/ListDetectionModule.ts create mode 100644 server/src/modules/Module.ts create mode 100644 server/src/modules/NumberCorrectionModule.ts create mode 100644 server/src/modules/OutOfPageRemovalModule.ts create mode 100644 server/src/modules/ReadingOrderDetectionModule.ts create mode 100644 server/src/modules/RedundancyDetectionModule.ts create mode 100644 server/src/modules/RegexMatcherModule.ts create mode 100644 server/src/modules/RemoteModule.ts create mode 100644 server/src/modules/SeparateWordsModule.ts create mode 100644 server/src/modules/TemplateModule.ts create mode 100644 server/src/modules/WhitespaceRemovalModule.ts create mode 100644 server/src/modules/WordsToLineModule.ts create mode 100644 server/src/tslint.conf create mode 100644 server/src/types/Config.ts create mode 100644 server/src/types/DocumentRepresentation/Barcode.ts create mode 100644 server/src/types/DocumentRepresentation/BoundingBox.ts create mode 100644 server/src/types/DocumentRepresentation/Character.ts create mode 100644 server/src/types/DocumentRepresentation/Color.ts create mode 100644 server/src/types/DocumentRepresentation/Document.ts create mode 100644 server/src/types/DocumentRepresentation/Drawing.ts create mode 100644 server/src/types/DocumentRepresentation/Element.ts create mode 100644 server/src/types/DocumentRepresentation/Font.ts create mode 100644 server/src/types/DocumentRepresentation/Heading.ts create mode 100644 server/src/types/DocumentRepresentation/Image.ts create mode 100644 server/src/types/DocumentRepresentation/JsonExport.ts create mode 100644 server/src/types/DocumentRepresentation/Line.ts create mode 100644 server/src/types/DocumentRepresentation/List.ts create mode 100644 server/src/types/DocumentRepresentation/Page.ts create mode 100644 server/src/types/DocumentRepresentation/Paragraph.ts create mode 100644 server/src/types/DocumentRepresentation/SvgLine.ts create mode 100644 server/src/types/DocumentRepresentation/SvgShape.ts create mode 100644 server/src/types/DocumentRepresentation/Table.ts create mode 100644 server/src/types/DocumentRepresentation/TableCell.ts create mode 100644 server/src/types/DocumentRepresentation/TableRow.ts create mode 100644 server/src/types/DocumentRepresentation/Text.ts create mode 100644 server/src/types/DocumentRepresentation/Word.ts create mode 100644 server/src/types/DocumentRepresentation/index.ts create mode 100644 server/src/types/Metadata/ComplexMetadata.ts create mode 100644 server/src/types/Metadata/KeyValueMetadata.ts create mode 100644 server/src/types/Metadata/Metadata.ts create mode 100644 server/src/types/Metadata/NumberMetadata.ts create mode 100644 server/src/types/Metadata/Properties.ts create mode 100644 server/src/types/Metadata/RegexMetadata.ts create mode 100644 server/src/types/Metadata/index.ts create mode 100644 server/src/types/Pdf2JsonFont.ts create mode 100644 server/src/types/Pdf2JsonPage.ts create mode 100644 server/src/types/Pdf2JsonText.ts create mode 100644 server/src/types/TableInfo.ts create mode 100644 server/src/types/TableReconstruction.ts create mode 100644 server/src/types/TsvElement.ts create mode 100644 server/src/utils.ts create mode 100644 server/src/utils/Logger.ts create mode 100644 server/src/utils/json2document.ts create mode 100644 sonar-project.properties create mode 100644 test/assets/html.pdf create mode 100644 test/assets/line-merge-2.pdf create mode 100644 test/assets/line-merge-2.pdf.json create mode 100644 test/assets/line-merge.pdf create mode 100644 test/assets/line-merge.pdf.json create mode 100644 test/assets/lists.pdf create mode 100644 test/assets/number-correction-1.pdf create mode 100644 test/assets/number-correction-1.pdf.json create mode 100644 test/assets/page-number.pdf create mode 100644 test/assets/page-numbers.pdf create mode 100644 test/assets/paragraph-merge-1.pdf create mode 100644 test/assets/paragraph-merge-1.pdf.json create mode 100644 test/assets/paragraph-merge.pdf create mode 100644 test/assets/paragraph-merge.pdf.json create mode 100644 test/assets/redundancy-detection.pdf create mode 100644 test/assets/text-order-detection.pdf create mode 100644 test/assets/text-order-detection.pdf.json create mode 100644 test/assets/text-order-mini.pdf create mode 100644 test/helpers.ts create mode 100644 test/json-export-import.spec.ts create mode 100644 test/line-merge.spec.ts create mode 100644 test/number-correction.spec.ts create mode 100644 test/paragraph-merge.spec.ts create mode 100644 test/redundancy-detection.spec.ts create mode 100644 test/text-order-detection.spec.ts create mode 100644 test/utils.spec.ts create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 00000000..78104786 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,159 @@ +--- +kind: pipeline +name: default + +platform: + os: linux + arch: amd64 + +node: + memory: high + +steps: +- name: Change file ownership + image: alpine:latest + commands: + - chown -R 1001:0 /drone/src + +- name: Build project + image: axarev/documentparser + environment: + LD_LIBRARY_PATH: /opt/rh/rh-nodejs8/root/usr/lib64 + NODE_ENV: development + commands: + - export PATH=/opt/rh/rh-nodejs8/root/usr/bin:$PATH + - npm install + +- name: Run formatter + image: axarev/documentparser + environment: + LD_LIBRARY_PATH: /opt/rh/rh-nodejs8/root/usr/lib64 + commands: + - export PATH=/opt/rh/rh-nodejs8/root/usr/bin:$PATH + - npm run format + +- name: Run linter + image: node:8 + commands: + - npm run lint + +- name: Run tests + image: axarev/documentparser + environment: + LD_LIBRARY_PATH: /opt/rh/rh-nodejs8/root/usr/lib64 + commands: + - export PATH=/opt/rh/rh-nodejs8/root/usr/bin:$PATH + - npm run test + +- name: Code-analysis + image: aosapps/drone-sonar-plugin:1.0 + settings: + sonar_host: + from_secret: sonar_host + sonar_token: + from_secret: sonar_token + when: + branch: + - master + +- name: Tag with demo + image: busybox + commands: + - echo demo > .tags + when: + branch: + - demo + +- name: Build Docker image + image: plugins/docker + settings: + repo: axarev/documentparser + context: . + dockerfile: docker/parsr/Dockerfile + username: + from_secret: registry_user + password: + from_secret: registry_password + build_args: + DEV_MODE: 'true' +# auto_tag: true + when: + branch: + - develop + - demo + event: + exclude: + - pull_request + +- name: Deploy dev + image: docker + environment: + DOCKER_HOST: + from_secret: docker_host + CA: + from_secret: docker_ca + CLIENT_CERT: + from_secret: docker_cert + CLIENT_KEY: + from_secret: docker_key + DOCKER_CERT_PATH: /cert + DOCKER_TLS_VERIFY: 1 + DOCKER_IMAGE: axarev/documentparser:latest + DOCKER_SERVICE: documentparser_documentparser-dev + REGISTRY_USER: + from_secret: registry_user + REGISTRY_PASSWORD: + from_secret: registry_password + commands: + - mkdir -p "$DOCKER_CERT_PATH" + - echo "$CA" > $DOCKER_CERT_PATH/ca.pem + - echo "$CLIENT_CERT" > $DOCKER_CERT_PATH/cert.pem + - echo "$CLIENT_KEY" > $DOCKER_CERT_PATH/key.pem + - docker login -u "$REGISTRY_USER" -p"$REGISTRY_PASSWORD" + - docker service update --with-registry-auth --image $DOCKER_IMAGE $DOCKER_SERVICE + - rm -rf $DOCKER_CERT_PATH + when: + branch: + - develop + - drone-ci + event: + exclude: + - pull_request + +- name: Deploy demo + image: docker + environment: + DOCKER_HOST: + from_secret: docker_host + CA: + from_secret: docker_ca + CLIENT_CERT: + from_secret: docker_cert + CLIENT_KEY: + from_secret: docker_key + DOCKER_CERT_PATH: /cert + DOCKER_TLS_VERIFY: 1 + DOCKER_IMAGE: axarev/documentparser:demo + DOCKER_SERVICE: documentparser_parsr-demo + REGISTRY_USER: + from_secret: registry_user + REGISTRY_PASSWORD: + from_secret: registry_password + commands: + - mkdir -p "$DOCKER_CERT_PATH" + - echo "$CA" > $DOCKER_CERT_PATH/ca.pem + - echo "$CLIENT_CERT" > $DOCKER_CERT_PATH/cert.pem + - echo "$CLIENT_KEY" > $DOCKER_CERT_PATH/key.pem + - docker login -u "$REGISTRY_USER" -p"$REGISTRY_PASSWORD" + - docker service update --with-registry-auth --image $DOCKER_IMAGE $DOCKER_SERVICE + - rm -rf $DOCKER_CERT_PATH + when: + branch: + - demo + event: + exclude: + - pull_request + + +image_pull_secrets: + - dockerconfigjson diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..457c36ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,108 @@ +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +yarn.lock + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# next.js build output +.next + +*dist* +samples +mutool-extraction +mutool-images +pipeline + + +# vscode settings +.vscode/settings.json + +# SonarQube +.sonar/ +.scannerwork/ + +# python / jupyter ignores +.ipynb_checkpoints +__pycache__ diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 00000000..e76ee99c --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,8 @@ +// Rationale about style choices can be found there https://prettier.io/docs/en/rationale.html + +module.exports = { + printWidth: 100, + singleQuote: true, + useTabs: true, + trailingComma: 'all', +}; diff --git a/.s2i/bin/assemble b/.s2i/bin/assemble new file mode 100755 index 00000000..49732740 --- /dev/null +++ b/.s2i/bin/assemble @@ -0,0 +1,20 @@ +#!/bin/bash + + +[ -x /usr/libexec/s2i/assemble ] && /usr/libexec/s2i/assemble + + +echo "" + +echo "Installing API" +npm run install:api + +echo + +echo "Installing Frontend (from demo)" +# "install:front": "npm install && npm run build:ts && npm run build:sass", +#npm run install:front +npm run build:ts +cd demo/web-viewer +npm install +npm run build:sass diff --git a/.s2i/bin/run b/.s2i/bin/run new file mode 100755 index 00000000..31cba1a2 --- /dev/null +++ b/.s2i/bin/run @@ -0,0 +1,3 @@ +#!/bin/bash + +exec /usr/libexec/s2i/run diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..e1f644a6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceRoot}/dist/bin/index.js", + "outFiles": ["${workspaceRoot}/dist/bin/**/*.js"], + "sourceMaps": true, + "args": [ + "-f", "${workspaceRoot}/samples/README.pdf", + "-o", "${workspaceRoot}/demo/web-viewer/pipeline/output", + "-n", "example", + "-c", "${workspaceRoot}/server/defaultConfig.json", + "-l", "debug", + "-p" + ], + "env": { + "NODE_DEBUG": "pipeline" + }, + "outputCapture": "std" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..49a7fedf --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "typescript", + "tsconfig": "tsconfig.json", + "problemMatcher": ["$tsc"], + "group": { + "kind": "build", + "isDefault": true, + } + } + ] +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..eae6537a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,61 @@ +# Contributing + +If you're looking to contribute to the project you're in the right place! + +Here's a quick guide to create a pull request: + +1. Fork the github project by visiting this URL: https://github.com/axa-group/Parsr/fork + +2. Clone the git repository + + $ git clone git@github.com:YOUR-GITHUB-USERNAME/Parsr.git + +3. Create a new branch in your git repository (branched from `develop` - see [Notes about branching](#notes-about-branching) below). + + $ cd Parsr/ + $ git checkout develop + $ git checkout -b issue/123-solve-the-issue # pick a better title + +4. Setup your build environment (see [build instructions in our README][readme]) and start hacking the project. You must follow our code style guidelines (we use `prettier`, checkout our [README][readme]), write good commit messages, comment your code and write automated tests. + +5. When your patch is ready, [submit a pull request][pr]. Add some comments or screen shots to help us. + +6. Wait for us to review your pull request. If something is wrong or if we want you to make some changes before the merge, we'll let you know through commit comments or pull request comments. + +[readme]: https://github.com/axa-group/Parsr/blob/develop/README.md +[pr]: https://github.com/axa-group/Parsr/compare/ + +# Versioning + +We're using [semantic versioning](https://semver.org/). + +Given a version number `MAJOR.MINOR.PATCH`, increment the: + +- **MAJOR** version when you make incompatible API changes, +- **MINOR** version when you add functionality in a backwards-compatible manner, and +- **PATCH** version when you make backwards-compatible bug fixes. + +# Branching + +We use the [git flow branching model][git-flow]. + +- `master` branch represents latest release version. HEAD of this branch should be equal to last tagged release. + +- `develop` branch represents the cutting edge version. This is probably the one you want to fork from and base your patch on. This is the default github branch. + +- Version tags. All released versions are tagged and pushed in the repository. For instance if you want to checkout the 1.0.2 version: + + $ git checkout 1.0.2 + +- Release branches. When a new version is going to be released, we'll branch from `develop` to `release/x.y`. This marks version x.y code freeze. Only blocking or major bug fixes will be merged to these branches. They represent beta and release candidates. + +- Hotfix branches. When one or several critical issues are found on current released version, we'll branch from `tags/x.y` to `hotfix/x.y.1` (or from `tags/x.y.z` to `hotfix/x.y.z+1` if a hotfix release has already been published). + +- Fix or feature branches. Proposed new features and bug fixes should live in their own branch. Use the following naming convention: if a github issue exists for this feature/bugfix, the branch will be named `issue/ISSUEID-comment` where ISSUEID is the corresponding github issue id. If a github issue doesn't exist, branch will be named `feature/comment`. These branches will be merged in: + - `hotfix/x.y.z` if the change is a fix for a released version + - `release/x.y` if the change is a fix for a beta or release candidate + - `develop` for all other cases + +Note: `release/x.y` or `hotfix/x.y.z` will be merged back in `master` after a new version is released. A new tag will be created and pushed at the same time. + +[git-flow]: http://nvie.com/posts/a-successful-git-branching-model/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..09968f31 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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 2019 AXA + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bf18009a --- /dev/null +++ b/README.md @@ -0,0 +1,331 @@ +[![Build Status](https://drone.rev.legal/api/badges/AXATechLab/AXA-AEL-pdfparser/status.svg)](https://drone.rev.legal/AXATechLab/AXA-AEL-pdfparser) + +# Parsr: Turn your documents into data! + +**Parsr**, is a minimal-footprint document (image, pdf) cleaning, parsing and extraction toolchain which generates readily available, organized and usable data for data scientists and developers. + +It provides users with clean structured and label-enriched information set for ready-to-use applications ranging from data entry and document analysis automation, archival, and many others. + +- [Parsr: Turn your documents into data!](#parsr-turn-your-documents-into-data) + - [1. Getting Started / Installation](#1-getting-started--installation) + - [1.1. Docker Installation](#11-docker-installation) + - [1.2. Bare-Metal Installation](#12-bare-metal-installation) + - [1.2.1. Installing Dependencies under Linux](#121-installing-dependencies-under-linux) + - [1.2.2. Installing Dependencies under MacOS](#122-installing-dependencies-under-macos) + - [1.2.3. Installing Dependencies under Windows](#123-installing-dependencies-under-windows) + - [1.2.3.1. pdf2json](#1231-pdf2json) + - [1.2.3.2. Tesseract](#1232-tesseract) + - [1.3. Optional Dependencies](#13-optional-dependencies) + - [1.3.1. MuPDF](#131-mupdf) + - [1.3.2. Pandoc](#132-pandoc) + - [1.3.3. ABBYY FineReader](#133-abbyy-finereader) + - [2. Usage](#2-usage) + - [2.1. Install npm packages](#21-install-npm-packages) + - [2.2. Run](#22-run) + - [2.2.1. Configuration](#221-configuration) + - [2.2.2. Demo: Web Viewer](#222-demo-web-viewer) + - [2.2.3. Command Line Usage](#223-command-line-usage) + - [2.3. API](#23-api) + - [2.4. Test](#24-test) + - [3. ABBYY FineReader Server](#3-abbyy-finereader-server) + - [3.1. Server Configuration](#31-server-configuration) + - [4. Dependencies Explanation](#4-dependencies-explanation) + - [4.1. Base Dependencies](#41-base-dependencies) + - [4.2. Extraction Dependencies](#42-extraction-dependencies) + - [4.3. Optional Dependencies](#43-optional-dependencies) + - [5. Contribute](#5-contribute) + - [6. Third Party Licenses](#6-third-party-licenses) + - [7. License](#7-license) + +## 1. Getting Started / Installation + +This section will quickly guide you through the installation process. + +You can install Parsr either using Docker containers, or directly on your machine. You don't need to do both! + +### 1.1. Docker Installation + +Containers are already avaiable on [Docker Hub](https://hub.docker.com/u/axarev). + +The documentation to build and run Docker containers is [here](docs/docker.md). + +### 1.2. Bare-Metal Installation + +#### 1.2.1. Installing Dependencies under Linux + +Under a **Debian** based distribution: + +```sh +sudo add-apt-repository ppa:ubuntuhandbook1/apps +sudo apt-get update +sudo apt-get install nodejs npm qpdf imagemagick pdf2json tesseract-ocr libtesseract-dev +``` + +Under **Arch** Linux : + +```sh +pacman -S nodejs npm qpdf imagemagick pdf2json tesseract +``` + +#### 1.2.2. Installing Dependencies under MacOS + +The package manager we suggest using under MacOS is [homebrew](https://brew.sh/). +To install it, launch the following in a terminal + +```sh +/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +Next, install the required dependencies: + +```sh +brew install node qpdf imagemagick pdf2json tesseract tesseract-lang +``` + +#### 1.2.3. Installing Dependencies under Windows + +1. We recommand using [Chocolatey](https://chocolatey.org) as the package manager for installing dependencies under Windows. To install Chocolatey, [follow these instructions](https://chocolatey.org/install#installing-chocolatey). + +2. Install **`qpdf`** and **`imagemagick`** using Powershell (Run as Administrator): + + ```sh + choco install qpdf imagemagick + ``` + +3. [Download and install **`node.js`**](https://nodejs.org/en/download) + +##### 1.2.3.1. pdf2json + +Download the latest release (`.msi` file) of pdf2json [here](https://github.com/flexpaper/pdf2json/releases). + +Then, you need to add `pdf2json.exe` to your PATH. +If you have install it in `C:\Program Files (x86)\PDF2JSON`, you can either add it [using the user interface](https://docs.alfresco.com/4.2/tasks/fot-addpath.html) or execute the following command in Powershell (Run as Administrator): + +```sh +setx PATH "\$env:PATH;C:\Program Files (x86)\PDF2JSON" -m +``` + +##### 1.2.3.2. Tesseract + +You can download Tesseract 4.0 64-bit for Windows or check out other available formats on [the wiki](https://github.com/UB-Mannheim/tesseract/wiki). + +Then, you need to add tesseract.exe to your PATH: +If you have install it in `C:\Program Files (x86)\Tesseract-OCR`, you can either add it [using the user interface](https://docs.alfresco.com/4.2/tasks/fot-addpath.html) execute the following command in Powershell (Run as Administrator): + +### 1.3. Optional Dependencies + +The following dependencies are **completely optional**, and their exclusion does not hinder the proper functioning of the Parsr pipeline. + +The functions of each, as well as the installation process are are explained below: + +#### 1.3.1. MuPDF + +MuPDF, in the Parsr platform is Used to fix certain error-prone or corrupt PDF files on input. + +To install MuPDF, follow the steps corresponding to your environment: + +- Under a **Debian** based distribution: + + ```sh + sudo apt-get install mupdf mupdf-tools + ``` + +- Under **Arch** Linux: + + ```sh + pacman -S mupdf-tools + ``` + +- Under MacOS: + + ```sh + brew install mupdf-tools + ``` + +- Under Windows: + + ```sh + choco install mupdf + ``` + +If MuPDF is not installed, a corrupt/unreadable PDF file at input will be left untreated. +A message of such an occurance will be logged. + +#### 1.3.2. Pandoc + +Pandoc is a document format conversion program, used under Parsr to generate PDF files from an intermediate Markdown output after the cleaning operation in the pipeline. + +To install Pandoc, follow the steps corresponding to your environment: + +- Under a **Debian** based distribution: + + ```sh + sudo apt-get install pandoc + ``` + +- Under **Arch** Linux: + + ```sh + pacman -S pandoc + ``` + +- Under MacOS: + + ```sh + brew install pandoc + ``` + +- Under Windows: + + ```sh + choco install pandoc + ``` + +If Pandoc is not installed, the user will not be able to generate PDF files on output. +Any configuration requiring a PDF file output will be ignored. + +#### 1.3.3. ABBYY FineReader + +ABBYY FineReader is a proprietary high precision OCR solution for generating rich text from images. +One can obtain the ABBYY FineReader Server from [here](https://www.abbyy.com/en-us/finereader-server/). + +ABBYY FineReader is an **optional dependency**, and it's absence should in no way hinder the everyday usage of Parsr's default OCR solution, tesseract. + +## 2. Usage + +You can use Parsr in different ways: + +- Using the command line +- Using the API +- Using the demo web viewer + +### 2.1. Install npm packages + +```sh +npm install +``` + +### 2.2. Run + +#### 2.2.1. Configuration + +The tool contains a pipeline of modules that process the document step by step and is highly configurable. To change it's default configuration, please refer to the [configuration file documentation](docs/configuration-file.md). + +#### 2.2.2. Demo: Web Viewer + +To start the web viewer demo, simply run: + +```sh +npm run start:web +``` + +Then, open [localhost:3000](http://localhost:3000) with your favorite browser. + +#### 2.2.3. Command Line Usage + +Under Mac OS X, Linux: + +```sh +npm run run:debug -- --input-file samples/t1.pdf --output-folder dist/ --document-name example --config server/defaultConfig.json --pretty-logs +``` + +Under Windows: + +```sh +cmd /C "npm run run:debug -- --input-file samples/t1.pdf --output-folder samples --document-name example --config server/defaultConfig.json --pretty-logs" +``` + +### 2.3. API + +Install the API server with: + +```sh +npm run install:api +``` + +And then start the API server with: + +```sh +npm run start:api +``` + +You can then call endpoints on [localhost:3001](http://localhost:3001). + +The documentation for the API can be found [here](docs/api-guide.md). + +### 2.4. Test + +```sh +npm run test +``` + +## 3. ABBYY FineReader Server + +The ABBYY FineReader is a high-precision OCR option provided to the users of the Parsr platform. +It is to be noted that it is completely optional, and that the default OCR solution supported under Parsr is tesseract, which is a dependency of the solution. + +### 3.1. Server Configuration + +When ABBYY FineReader Server is chosen as the working OCR extraction solution, the following environment variables need to be set on the host running Parsr: + +1. `ABBYY_SERVER_URL` : The network address of the ABBYY FineReader Server. +2. `ABBYY_SERVER_VER` : The major version number of the ABBYY FineReader Server. For example: 14 for ABBYY FineReader Server 14.01. +3. `ABBYY_WORKFLOW` : The name of the server 's workflow to be called to process the file. + +On the side of the ABBYY FineReader Server, make sure the XML output is configured for the selected workflow: + +1. Double click on the workflow to be used. +2. In the tab titled 'output', make sure the list of file formats exported contains the XML format if not, add it with the 'New' button. +3. Make sure the following settings are enabled on the XML format 's settings: + 1. Character Attributes + 2. Extended Character Attributes + 3. Coordinates of the Original Image + 4. Character Formatting + +## 4. Dependencies Explanation + +### 4.1. Base Dependencies + +The following _required_ dependencies need to be installed for Parsr to work properly: + +1. `node.js` : The underlying framework upon which the platform is built. +2. `qpdf` : For reading password-protected PDFs. +3. `imagemagick` : For converting between file formats. + +### 4.2. Extraction Dependencies + +Depending upon the type of documents to be treated by the platform, one or multiple of the following dependencies should be installed. + +If simple PDFs containing digital (or _selectable_) textual elements are to be fed into the system, the **`pdf2json`** library needs to be installed. + +If images (`jpg`, `png`, `tiff`, etc.) are to be used with the tool, then the tool also supports the use of the following two OCR based solutions as an underlying extraction module: + +1. **`tesseract`** : Open source, support for over ~100 languages, Google's Tesseract is a free, on premise OCR solution. However, text formatting, or tabular data is not detected. +2. **`ABBYY FineReader Server`** : Proprietary OCR solution with extremely high recognition accuracy, formatting recognition and tabular data extraction. It is an optional dependency. + +### 4.3. Optional Dependencies + +The following _optional_ dependencies may to be installed: + +1. `mupdf-tools`: For error-correcting corrupt PDF's at input. +2. `pandoc`: Generate PDF files from an intermediate Markdown output after the cleaning operation in the pipeline. + +## 5. Contribute + +Please refer to the guidelines in [CONTRIBUTING.md](CONTRIBUTING.md). + +## 6. Third Party Licenses + +Third Party Libraries licenses : + +1. **QPDF**: Apache [http://qpdf.sourceforge.net](http://qpdf.sourceforge.net/) +2. **ImageMagick**: Apache 2.0 [https://imagemagick.org/script/license.php](https://imagemagick.org/script/license.php) +3. **Pdf2json**: Apache 2.0 [https://github.com/modesty/pdf2json/blob/scratch/quadf-forms/license.txt](https://github.com/modesty/pdf2json/blob/scratch/quadf-forms/license.txt) +4. **Tesseract**: Apache 2.0 [https://github.com/tesseract-ocr/tesseract](https://github.com/tesseract-ocr/tesseract) +5. **Camelot**: MIT [https://github.com/camelot-dev/camelot](https://github.com/camelot-dev/camelot) +6. **MuPDF** (Optional dependency): AGPL [https://mupdf.com/license.html](https://mupdf.com/license.html) +7. **Pandoc** (Optional dependency): GPL [https://github.com/jgm/pandoc](https://github.com/jgm/pandoc) + +## 7. License + +Copyright (C) 2019 AXA. Licensed under the [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) license (see the [LICENSE](LICENSE) file). diff --git a/api/server/package-lock.json b/api/server/package-lock.json new file mode 100644 index 00000000..0a9289b5 --- /dev/null +++ b/api/server/package-lock.json @@ -0,0 +1,832 @@ +{ + "name": "parsr-api", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/body-parser": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz", + "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.16.1.tgz", + "integrity": "sha512-V0clmJow23WeyblmACoxbHBu2JKlE5TiIme6Lem14FnPW9gsttyHtk6wq7njcdIWH1njAaFgR8gW09lgY98gQg==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.1.tgz", + "integrity": "sha512-QgbIMRU1EVRry5cIu1ORCQP4flSYqLM1lS5LYyGWfKnFT3E58f0gKto7BR13clBFVrVZ0G0rbLZ1hUpSkgQQOA==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/multer": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.3.7.tgz", + "integrity": "sha512-Lx4rNtGajRGtcVwJe1sKPAkAuBBWq8TOuimKJfOfK7ayY1Jc+18Lx00GjagLeIwaH2+OvFJvCv8tz+pvbt3OoA==", + "requires": { + "@types/express": "*" + } + }, + "@types/node": { + "version": "11.11.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.3.tgz", + "integrity": "sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==" + }, + "@types/pino": { + "version": "5.8.6", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-5.8.6.tgz", + "integrity": "sha512-3hKjgaAXi8FALboVw1LC+3wiQN+bLZ/byzVgRI65XZxdVLSgjIl3kMVh2etKEdQ96qUgMd6bhNtzrUwh1e1x9g==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/sonic-boom": "*" + } + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "@types/sonic-boom": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@types/sonic-boom/-/sonic-boom-0.6.2.tgz", + "integrity": "sha512-vP9Sn1tuz/BTh8L1o776Cbzr+WH4dZGmRXOjQ5L+IVQx40hUmvOS2wfIkqUsID1vL62tThWdlXWIqijwewu3mw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "accepts": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", + "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", + "requires": { + "mime-types": "~2.1.18", + "negotiator": "0.6.1" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" + }, + "fast-redact": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-1.5.0.tgz", + "integrity": "sha512-Afo61CgUjkzdvOKDHn08qnZ0kwck38AOGcMlvSGzvJbIab6soAP5rdoQayecGCDsD69AiF9vJBXyq31eoEO2tQ==" + }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + } + }, + "flatstr": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.9.tgz", + "integrity": "sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw==" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", + "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", + "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz", + "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, + "negotiator": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", + "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "parseurl": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", + "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "pino": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-5.12.2.tgz", + "integrity": "sha512-EunVRDkw/eQzgAudJiZuqzEQ20hDezixLRLrdxUMBzavvt5ot3vep7K8swRvXSgj2bKtbOmoHnrRMtYzRjfITQ==", + "requires": { + "fast-redact": "^1.4.4", + "fast-safe-stringify": "^2.0.6", + "flatstr": "^1.0.9", + "pino-std-serializers": "^2.3.0", + "quick-format-unescaped": "^3.0.2", + "sonic-boom": "^0.7.3" + } + }, + "pino-pretty": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-2.6.1.tgz", + "integrity": "sha512-e/CWtKLidqkr7sinfIVVcsfcHgnFVlGvuEfKuuPFnxBo+9dZZsmgF8a9Rj7SYJ5LMZ8YBxNY9Ca46eam4ajKtQ==", + "requires": { + "args": "^5.0.0", + "chalk": "^2.3.2", + "dateformat": "^3.0.3", + "fast-json-parse": "^1.0.3", + "fast-safe-stringify": "^2.0.6", + "jmespath": "^0.15.0", + "pump": "^3.0.0", + "readable-stream": "^3.0.6", + "split2": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "pino-std-serializers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.4.0.tgz", + "integrity": "sha512-ysT2ylXu1aEec9k8cm/lz7emBcfpdxFWHqvHeGXf1wvfw7TKPMGhLWwS+ciHw6u4ffnmV+pkAMF4MUIZmZZdSg==" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "proxy-addr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz", + "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.8.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "quick-format-unescaped": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.2.tgz", + "integrity": "sha512-FXTaCkwvpIlkdKeGDNgcq07SXWS383noQUuZjvdE1QcTt+eLuqof6/BDiEPqB59FWLie/l91+HtlJSw7iCViSA==" + }, + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", + "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.6.2", + "mime": "1.4.1", + "ms": "2.0.0", + "on-finished": "~2.3.0", + "range-parser": "~1.2.0", + "statuses": "~1.4.0" + } + }, + "serve-static": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", + "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.2", + "send": "0.16.2" + } + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "sonic-boom": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-0.7.3.tgz", + "integrity": "sha512-A9EyoIeLD+g9vMLYQKjNCatJtAKdBQMW03+L8ZWWX/A6hq+srRCwdqHrBD1R8oSMLXov3oHN13dljtZf12q2Ow==", + "requires": { + "flatstr": "^1.0.9" + } + }, + "split2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz", + "integrity": "sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==", + "requires": { + "readable-stream": "^3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "type-is": { + "version": "1.6.16", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", + "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.18" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/api/server/package.json b/api/server/package.json new file mode 100644 index 00000000..d5a6e663 --- /dev/null +++ b/api/server/package.json @@ -0,0 +1,29 @@ +{ + "name": "parsr-api", + "version": "1.0.0", + "description": "API for Parsr", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "typings": "dist/index.d.ts", + "scripts": { + "watch": "tsc -p tsconfig.json -w", + "build": "tsc -p tsconfig.json", + "start": "npm run build && node ./dist/index.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Sacha Bron", + "license": "ISC", + "dependencies": { + "@types/multer": "^1.3.7", + "commander": "^2.19.0", + "express": "^4.16.4", + "multer": "^1.4.1", + "pino": "^5.12.0", + "pino-pretty": "^2.6.0" + }, + "devDependencies": { + "@types/express": "^4.16.1", + "@types/node": "^11.11.1", + "@types/pino": "^5.8.6" + } +} diff --git a/api/server/src/FileManager.ts b/api/server/src/FileManager.ts new file mode 100644 index 00000000..fcceb776 --- /dev/null +++ b/api/server/src/FileManager.ts @@ -0,0 +1,112 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { Binder, FileMapper, OutputConfig, SingleFileType } from './types'; + +export class FileManager { + private fileSystem: FileMapper = {}; + + public newBinder( + docId: string, + document: string, + configPath: string, + outputPath: string, + docName: string, + ): Binder { + const binder: Binder = { + input: document, + name: docName, + outputPath, + config: configPath, + }; + + this.fileSystem[docId] = binder; + + return this.fileSystem[docId]; + } + + public getCsvFilePath(docId: string, page: number, table: number) { + const binder: Binder = this.getBinder(docId); + + const absPath: string = path.resolve( + `${binder.outputPath}/csv/${binder.name}-${page}-${table}.csv`, + ); + + if (!fs.existsSync(absPath)) { + throw new Error(`File not found for document ID ${docId}`); + } + + return absPath; + } + + public getFilePath(docId: string, type: SingleFileType | 'csvs'): string { + const binder = this.getBinder(docId); + + if (type === 'json') { + return this.checkFile(binder, `${binder.name}.json`); + } + + if (type === 'markdown') { + return this.checkFile(binder, `${binder.name}.md`); + } + + if (type === 'pdf') { + return this.checkFile(binder, `${binder.name}.pdf`); + } + + if (type === 'text') { + return this.checkFile(binder, `${binder.name}.txt`); + } + + if (type === 'xml') { + return this.checkFile(binder, `${binder.name}.xml`); + } + + if (type === 'confidances') { + return this.checkFile(binder, `${binder.name}-confidances.txt`); + } + + if (type === 'csvs') { + const absPath = path.resolve(`${binder.outputPath}/csv`); + + if (!fs.existsSync(absPath)) { + throw new Error(`Folder of CSV files not found for document ID ${docId}`); + } + + return absPath; + } + } + + private getBinder(docId: string): Binder { + if (this.fileSystem[docId]) { + return this.fileSystem[docId]; + } else { + throw new Error(`Binder with Document ID ${docId} not found.`); + } + } + + private checkFile(binder: Binder, filePath: string): string { + const absPath = path.resolve(`${binder.outputPath}/${filePath}`); + + if (!fs.existsSync(absPath)) { + throw new Error(`File not found`); + } + + return absPath; + } +} diff --git a/api/server/src/Logger.ts b/api/server/src/Logger.ts new file mode 100644 index 00000000..d7fb5701 --- /dev/null +++ b/api/server/src/Logger.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import pino from 'pino'; + +const logger = pino({ + name: 'parsr-api', + prettyPrint: { colorize: true, translateTime: "yyyy-mm-dd'T'HH:MM:ss" }, + level: 'info', +}); + +export default logger; diff --git a/api/server/src/ProcessManager.ts b/api/server/src/ProcessManager.ts new file mode 100644 index 00000000..dc83946b --- /dev/null +++ b/api/server/src/ProcessManager.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawn } from 'child_process'; +import * as path from 'path'; +import logger from './Logger'; +import { PipelineProcess, ProcessMapper } from './types'; + +export class ProcessManager { + private processes: ProcessMapper = {}; + + public start( + doc: string, + docId: string, + config: string, + docName: string, + outputPath: string, + ): void { + logger.info('Processing ' + doc); + + process.env.NODE_DEBUG = 'pipeline'; + + const args: string[] = [ + `../../dist/bin/index.js`, + '--input-file', + path.resolve(doc), + '--output-folder', + path.resolve(outputPath), + '--document-name', + docName, + '--config', + path.resolve(config), + ]; + + logger.info('node', args.join(' ')); + + const pipelineProcess: PipelineProcess = { + childProcess: spawn(`node`, args, { + env: process.env, + }), + isDone: false, + stdout: [], + stderr: [], + start: new Date(), + }; + + pipelineProcess.childProcess.stdout.on('data', data => { + pipelineProcess.stdout.push(data.toString('utf-8')); + logger.info(data.toString('utf-8')); + }); + + pipelineProcess.childProcess.stderr.on('data', data => { + pipelineProcess.stderr.push(data.toString('utf-8')); + logger.error(data.toString('utf-8')); + }); + + pipelineProcess.childProcess.on('exit', code => { + pipelineProcess.isDone = true; + pipelineProcess.exitCode = code; + logger.info('Process exited'); + }); + + this.processes[docId] = pipelineProcess; + } + + public getProcess(docId: string): PipelineProcess { + if (this.processes[docId]) { + return this.processes[docId]; + } else { + throw new Error(`Process with Document ID ${docId} not found.`); + } + } +} diff --git a/api/server/src/api.ts b/api/server/src/api.ts new file mode 100644 index 00000000..a6aa1dbf --- /dev/null +++ b/api/server/src/api.ts @@ -0,0 +1,292 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as crypto from 'crypto'; +import express from 'express'; +import { Request, Response } from 'express-serve-static-core'; +import * as fs from 'fs'; +import multer from 'multer'; +import * as os from 'os'; +import * as path from 'path'; +import { FileManager } from './FileManager'; +import logger from './Logger'; +import { ProcessManager } from './ProcessManager'; +import { Binder, PipelineProcess, QueueStatus, SingleFileType } from './types'; + +export class ApiServer { + private outputDir: string = path.resolve(`${__dirname}/output`); + private fileManager: FileManager = new FileManager(); + private processManager: ProcessManager = new ProcessManager(); + + private upload = multer({ + storage: multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, this.getRandomFolder()); + }, + filename: (req, file, cb) => { + const ext = file.originalname.split('.').pop(); + cb(null, path.basename(this.getRandomFile(`.${ext}`))); + }, + }), + }); + + private allowedMimetypes: string[] = [ + 'application/pdf', + 'application/xml', + 'image/tiff', + 'image/png', + 'image/jpeg', + ]; + + constructor() { + if (!fs.existsSync(this.outputDir)) { + fs.mkdirSync(this.outputDir); + } + } + + public launchServer(port: number): void { + const app = express(); + + // tslint:disable-next-line:variable-name + const v1_0 = express.Router(); + app.use('/api/v1.0', v1_0); + app.use('/api/v1', v1_0); + app.use('/api', v1_0); + + app.get('/', this.handleRoot.bind(this)); + + const uploadsConf: multer.Field[] = [ + { name: 'file', maxCount: 1 }, + { name: 'config', maxCount: 1 }, + ]; + + v1_0.post('/document', this.upload.fields(uploadsConf), this.handlePostDoc.bind(this)); + v1_0.get('/queue/:id', this.handleGetQueue.bind(this)); + v1_0.get('/json/:id', this.handleGetJson.bind(this)); + v1_0.get('/text/:id', this.handleGetText.bind(this)); + v1_0.get('/confidances/:id', this.handleGetConfidances.bind(this)); + v1_0.get('/csv/:id', this.handleGetCsvList.bind(this)); + v1_0.get('/csv/:id/:page/:table', this.handleGetCsv.bind(this)); + v1_0.get('/markdown/:id', this.handleGetMarkdown.bind(this)); + v1_0.get('/xml/:id', this.handleGetXml.bind(this)); + // TODO add every other endpoint + + app.listen(port, () => { + logger.info(`Api listening on port ${port}!`); + }); + } + + /** + * Status: 200 - Ok. Returns the status of the queue + * Status: 201 - Created. Returns the id of the document and links to generated resources. + * Status: 404 - Not Found + * Status: 500 - Internal Server Error + */ + private handleGetQueue(req: Request, res: Response): void { + const docId = req.params.id; + let pipelineProcess: PipelineProcess; + + try { + pipelineProcess = this.processManager.getProcess(docId); + } catch (err) { + res.sendStatus(404); + return; + } + + if (pipelineProcess.isDone) { + if (pipelineProcess.exitCode === 0) { + const paths = { + id: docId, + json: `${req.baseUrl}/json/${docId}`, + csv: `${req.baseUrl}/csv/${docId}`, + text: `${req.baseUrl}/text/${docId}`, + markdown: `${req.baseUrl}/markdown/${docId}`, + }; + + res + .status(201) + .location(paths.json) + .json(paths); + } else { + res.status(500).send(pipelineProcess.stderr.join('')); + } + } else { + let status = ''; + + if (pipelineProcess.stderr.length > 0) { + status = this.processStatus(pipelineProcess.stderr[pipelineProcess.stderr.length - 1]); + } else if (pipelineProcess.stdout.length > 0) { + status = this.processStatus(pipelineProcess.stdout[pipelineProcess.stdout.length - 1]); + } + + const queueStatus: QueueStatus = { + 'estimated-remaining-time': null, + 'progress-percentage': 0, + 'start-date': pipelineProcess.start, + status, + }; + + res.json(queueStatus); + } + } + + private processStatus(status: string): string { + try { + return JSON.parse(status).msg; + } catch (e) { + return status; + } + } + + /* + { + fieldname: 'file', + originalname: 't1_bis.pdf', + encoding: '7bit', + mimetype: 'application/pdf', + destination: 'uploads', + filename: 'cbfa07119f4df59b76ba0126b5f5c4cf', + path: 'uploads/cbfa07119f4df59b76ba0126b5f5c4cf', + size: 108284 + } + */ + private handlePostDoc(req: Request, res: Response): void { + if (!('files' in req && 'file' in req.files && 'config' in req.files)) { + res.status(400).send(`Bad request: file or config not found.`); + return; + } + + const doc: Express.Multer.File = req.files.file[0]; + const config: Express.Multer.File = req.files.config[0]; + const docName: string = doc.originalname.split('.')[0]; + const docId: string = this.getUUID(); + const outputPath = path.resolve(`${this.outputDir}/${docName}-${docId}`); + + if (!this.isValidDocument(doc) || !this.isValidConfig(config)) { + res.sendStatus(415); + return; + } + + try { + fs.mkdirSync(outputPath); + } catch (err) { + res.status(500).send(err); + return; + } + + this.fileManager.newBinder(docId, doc.path, config.path, outputPath, docName); + this.processManager.start(doc.path, docId, config.path, docName, outputPath); + + res + .status(202) + .location(`${req.baseUrl}/queue/${docId}`) + .send(docId); + } + + private handleGetJson(req: Request, res: Response): void { + this.handleGetFile(req, res, 'json'); + } + + private handleGetText(req: Request, res: Response): void { + this.handleGetFile(req, res, 'text'); + } + + private handleGetConfidances(req: Request, res: Response) { + this.handleGetFile(req, res, 'confidances'); + } + + private handleGetCsv(req: Request, res: Response) { + const docId: string = req.params.id; + const page: number = parseInt(req.params.page, 10); + const table: number = parseInt(req.params.table, 10); + + try { + const file: string = this.fileManager.getCsvFilePath(docId, page, table); + res.sendFile(file); + } catch (err) { + res.status(404).send(err); + } + } + + private handleGetCsvList(req: Request, res: Response) { + const docId: string = req.params.id; + const folder: string = this.fileManager.getFilePath(docId, 'csvs'); + const paths: string[] = fs.readdirSync(folder).map(filename => { + const match = filename.match(/-(\d+)-(\d+)\.csv$/); + return `${req.baseUrl}/csv/${docId}/${match[1]}/${match[2]}`; + }); + + res.json(paths); + } + + private handleGetMarkdown(req: Request, res: Response) { + this.handleGetFile(req, res, 'markdown'); + } + + private handleGetXml(req: Request, res: Response) { + this.handleGetFile(req, res, 'xml'); + } + + private handleGetFile(req: Request, res: Response, type: SingleFileType): void { + try { + const file: string = this.fileManager.getFilePath(req.params.id, type); + res.sendFile(file); + } catch (err) { + res.status(404).send(err); + } + } + + private handleRoot(req: Request, res: Response): void { + res.send(` +

+ Welcome to the API.
+ Are you lost? Checkout the + documentation! +

+ `); + } + + private isValidDocument(doc: Express.Multer.File): boolean { + if (!this.allowedMimetypes.includes(doc.mimetype)) { + return false; + } + + return true; + } + + private isValidConfig(config: Express.Multer.File): boolean { + if (config.mimetype !== 'application/json') { + return false; + } + + return true; + } + + private getRandomFolder(): string { + const randFoldername = `${os.tmpdir()}/${this.getUUID()}`; + fs.mkdirSync(randFoldername); + return path.resolve(`${randFoldername}`); + } + + private getRandomFile(extension: string): string { + const randFilename = `${os.tmpdir()}/${this.getUUID() + extension}`; + return path.resolve(`${randFilename}`); + } + + private getUUID(): string { + return crypto.randomBytes(15).toString('hex'); + } +} diff --git a/api/server/src/index.ts b/api/server/src/index.ts new file mode 100644 index 00000000..99f08100 --- /dev/null +++ b/api/server/src/index.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiServer } from './api'; + +const api = new ApiServer(); + +api.launchServer(3001); diff --git a/api/server/src/types.ts b/api/server/src/types.ts new file mode 100644 index 00000000..26f2de1a --- /dev/null +++ b/api/server/src/types.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ChildProcess } from 'child_process'; + +export type FileMapper = { + [docId: string]: Binder; +}; + +export type ProcessMapper = { + [docId: string]: PipelineProcess; +}; + +export type PipelineProcess = { + childProcess: ChildProcess; + isDone: boolean; + exitCode?: number; + start: Date; + stdout: string[]; + stderr: string[]; +}; + +export type Binder = BinderFiles & BinderKeys; + +type BinderFiles = { [key in SingleFileType]?: string }; + +type BinderKeys = { + csvs?: string[][]; + outputPath: string; + name: string; +}; + +export type SingleFileType = + | 'config' + | 'input' + | 'json' + | 'xml' + | 'text' + | 'pdf' + | 'markdown' + | 'confidances'; + +export type QueueStatus = { + 'progress-percentage': number; + status: string; + 'start-date': Date; + 'estimated-remaining-time': number; +}; + +export type OutputGranularityOptions = 'character' | 'word'; +export type OutputConfig = { + granularity: OutputGranularityOptions; + formats: { + json?: boolean; + 'json-compact'?: boolean; + text?: boolean; + markdown?: boolean; + xml?: boolean; + confidances?: boolean; + csv?: boolean; + pdf?: boolean; + }; +}; diff --git a/api/server/tsconfig.json b/api/server/tsconfig.json new file mode 100644 index 00000000..f0c5c5a9 --- /dev/null +++ b/api/server/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "outDir": "dist", + "target": "es5", + "moduleResolution": "node", + "noImplicitAny": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "declaration": true, + "sourceMap": true, + "lib": ["es7"] + }, + "exclude": ["public", "dist"] +} diff --git a/api/swagger/openapi.yaml b/api/swagger/openapi.yaml new file mode 100644 index 00000000..913481ff --- /dev/null +++ b/api/swagger/openapi.yaml @@ -0,0 +1,436 @@ +openapi: '3.0.1' +info: + description: '' + version: '1.0.0' + title: 'Parsr API' + contact: + email: 'sacha.bron.ats@axa.com' + name: Parsr + url: https://github.com/axa-group/Parsr + +servers: + - url: 'https://localhost:3001/api/v1' + description: 'Localhost server' + +paths: + /document: + post: + operationId: postDocument + tags: + - 'Input' + summary: Pipeline Input + description: 'Entry point to add a file to the processing queue.' + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + config: + $ref: '#/components/schemas/config' + responses: + '202': + description: Accepted + content: + text/plain: + schema: + type: string + example: '{id}' + headers: + Location: + description: Path to the queue for the given job. + schema: + type: string + example: /api/v1.0/queue/{id} + '415': + description: Unsupported Media Type + + /queue/{id}: + get: + operationId: getQueueStatus + tags: + - 'Processing' + summary: 'Get the status of the queue' + description: 'Get the status of the queue' + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Ok. Returns the status of the queue + content: + application/json: + schema: + $ref: '#/components/schemas/queueStatus' + '201': + description: Created. Returns the id of the document and links to generated resources. + headers: + Location: + description: Path to the primary output data + schema: + type: string + example: /api/v1.0/json/{id} + content: + application/json: + schema: + type: object + example: + { + id: '{id}', + json: '/api/v1.0/json/{id}', + markdown: '/api/v1.0/markdown/{id}', + csv: '/api/v1.0/csv/{id}', + } + properties: + id: + type: string + json: + type: string + xml: + type: string + markdown: + type: string + text: + type: string + confidences: + type: string + json-compact: + type: string + csv: + type: string + '404': + description: Not Found + '500': + description: Internal Server Error + + /json/{id}: + get: + operationId: getJson + tags: + - 'Output' + summary: 'Get the JSON representation of the document' + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/json' + '404': + description: Not Found + + # /xml/{id}: + # get: + # operationId: getXml + # tags: + # - "Output" + # summary: "Get the XML representation of the document" + # parameters: + # - $ref: '#/components/parameters/id' + # responses: + # "200": + # description: Ok + # content: + # application/xml: + # schema: + # type: array + # items: + # $ref: '#/components/schemas/json' + # xml: + # name: document + # "404": + # description: Not Found + + /text/{id}: + get: + operationId: getText + tags: + - 'Output' + summary: 'Get the raw text representation of the document' + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Ok + content: + text/plain: + schema: + type: string + example: Lorem ipsum dolor sit amet, consectetur adipiscing elit. + '404': + description: Not Found + + # /confidences/{id}: + # get: + # operationId: getConfidences + # tags: + # - "Output" + # summary: "Get the confidences of every raw text words" + # parameters: + # - $ref: '#/components/parameters/id' + # responses: + # "200": + # description: Ok + # content: + # text/plain: + # schema: + # type: string + # example: + # 100 98.5 98 95 98.2 90 93 99.8 + # "404": + # description: Not Found + + # /json-compact/{id}: + # get: + # operationId: getJsonCompact + # tags: + # - "Output" + # summary: "Get the JSON representation of the document in a more compact format (to be implemented)" + # parameters: + # - $ref: '#/components/parameters/id' + # responses: + # "501": + # description: Not Implemented + + /markdown/{id}: + get: + operationId: getMarkdown + tags: + - 'Output' + summary: 'Get the Markdown representation of the document' + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Ok + content: + text/markdown: + schema: + type: string + example: '## Lorem ipsum dolor sit amet, _consectetur adipiscing_ elit.' + '404': + description: Not Found + + /csv/{id}: + get: + operationId: getCsvList + tags: + - 'Output' + summary: 'Get the list of every CSV file path' + parameters: + - $ref: '#/components/parameters/id' + responses: + '200': + description: Ok + content: + application/json: + schema: + type: array + items: + type: string + description: Paths to CSV files + example: ['/api/v1.0/csv/{id}/1/1', '/api/v1.0/csv/{id}/{page}/{table}'] + '404': + description: Not Found + + /csv/{id}/{page}/{table}: + get: + operationId: getCsv + tags: + - 'Output' + summary: 'Get the CSV representation of a table' + parameters: + - $ref: '#/components/parameters/id' + - name: page + in: path + description: Page number + required: true + schema: + type: number + - name: table + in: path + description: Table number + required: true + schema: + type: number + + responses: + '200': + description: Ok + content: + text/csv: + schema: + type: string + example: | + 3x4 table;Empty column;Numbers + ;; + Item A;;3.14 + "Item B + on two lines";;1,234.56 + '404': + description: Not Found + +components: + parameters: + id: + name: id + in: path + description: ID of the document + required: true + schema: + type: string + + schemas: + queueStatus: + type: object + properties: + progress-percentage: + type: number + example: 25 + status: + type: string + example: 'detecting reading order' + start-date: + type: string + format: date-time + estimated-remaining-time: + type: number + example: 60 + + config: + type: object + properties: + version: + type: number + extractor: + type: object + properties: + pdf: + type: string + enum: ['pdf2json', 'tesseract', 'abbyy'] + img: + type: string + enum: ['tesseract', 'abbyy'] + language: + type: string + cleaner: + oneOf: + - type: string + - type: object + output: + type: object + properties: + granularity: + type: string + enum: ['word', 'character', 'line', 'paragraph'] + formats: + type: object + properties: + json: + type: boolean + #json-compact: + # type: boolean + text: + type: boolean + markdown: + type: boolean + #xml: + # type: boolean + #confidences: + # type: boolean + csv: + type: boolean + #pdf: + # type: boolean + example: + verison: '1.0' + + element: + type: object + properties: + id: + type: number + xml: + attribute: true + type: + type: string + enum: + [ + character, + drawing, + heading, + image, + list, + paragraph, + table, + table-cell, + table-row, + word, + line, + barcode, + ] + xml: + attribute: true + box: + $ref: '#/components/schemas/box' + content: + type: array + items: + $ref: '#/components/schemas/element' + box: + type: object + properties: + l: + type: number + t: + type: number + w: + type: number + h: + type: number + json: + type: object + properties: + metadata: + type: array + items: + type: object + properties: + id: + type: number + xml: + attribute: true + order: + type: number + pages: + type: array + items: + type: object + properties: + pageNumber: + type: number + xml: + attribute: true + box: + $ref: '#/components/schemas/box' + elements: + type: array + items: + $ref: '#/components/schemas/element' + fonts: + type: array + items: + type: object + properties: + id: + type: number + xml: + attribute: true + name: + type: string + size: + type: number diff --git a/api/swagger/swagger-generated.yaml b/api/swagger/swagger-generated.yaml new file mode 100755 index 00000000..09745e19 --- /dev/null +++ b/api/swagger/swagger-generated.yaml @@ -0,0 +1,518 @@ +openapi: 3.0.1 +info: + title: Parsr API + contact: + name: Parsr + url: https://github.com/axa-group/Parsr + email: sacha.bron.ats@axa.com + version: 1.0.0 +servers: + - url: https://localhost:3000/api/v1 + description: Localhost server +paths: + /document: + post: + tags: + - Input + summary: Pipeline Input + description: Entry point to add a file to the processing queue. + operationId: postDocument + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/body' + responses: + 202: + description: Accepted + headers: + Location: + description: Path to the queue for the given job. + style: simple + explode: false + schema: + type: string + example: https://doctech.ael.li/api/v1/queue/{id} + 415: + description: Unsupported Media Type + x-swagger-router-controller: Input + /queue/{id}: + get: + tags: + - Processing + summary: Get the status of the queue + description: Get the status of the queue + operationId: getQueueStatus + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok. Returns the status of the queue + content: + application/json: + schema: + $ref: '#/components/schemas/queueStatus' + 201: + description: Created. Returns the id of the document and links to generated resources. + headers: + Location: + description: Path to the primary output data + style: simple + explode: false + schema: + type: string + example: https://doctech.ael.li/api/v1/json/{id} + content: + application/json: + schema: + $ref: '#/components/schemas/inline_response_201' + 404: + description: Not Found + 500: + description: Internal Server Error + x-swagger-router-controller: Processing + /json/{id}: + get: + tags: + - Output + summary: Get the JSON representation of the document + operationId: getJson + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/json' + 404: + description: Not Found + x-swagger-router-controller: Output + /xml/{id}: + get: + tags: + - Output + summary: Get the XML representation of the document + operationId: getXml + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + application/xml: + schema: + type: array + xml: + name: document + items: + $ref: '#/components/schemas/json' + 404: + description: Not Found + x-swagger-router-controller: Output + /text/{id}: + get: + tags: + - Output + summary: Get the raw text representation of the document + operationId: getText + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + text/plain: + schema: + type: string + example: Lorem ipsum dolor sit amet, consectetur adipiscing elit. + 404: + description: Not Found + x-swagger-router-controller: Output + /confidances/{id}: + get: + tags: + - Output + summary: Get the confidances of every raw text words + operationId: getConfidances + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + text/plain: + schema: + type: string + example: 100 98.5 98 95 98.2 90 93 99.8 + 404: + description: Not Found + x-swagger-router-controller: Output + /json-compact/{id}: + get: + tags: + - Output + summary: Get the JSON representation of the document in a more compact format (to be implemented) + operationId: getJsonCompact + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 501: + description: Not Implemented + x-swagger-router-controller: Output + /markdown/{id}: + get: + tags: + - Output + summary: Get the Markdown representation of the document + operationId: getMarkdown + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + text/markdown: + schema: + type: string + example: '## Lorem ipsum dolor sit amet, _consectetur adipiscing_ + elit.' + 404: + description: Not Found + x-swagger-router-controller: Output + /csv/{id}: + get: + tags: + - Output + summary: Get the list of every CSV file path + operationId: getCsvList + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + responses: + 200: + description: Ok + content: + application/json: + schema: + type: array + example: + - https://dochtech.ael.li/api/v1/csv/{id}/1/1 + - https://dochtech.ael.li/api/v1/csv/{id}{page}/{table} + items: + type: string + description: Paths to CSV files + 404: + description: Not Found + x-swagger-router-controller: Output + /csv/{id}/{page}/{table}: + get: + tags: + - Output + summary: Get the CSV representation of a table + operationId: getCsv + parameters: + - name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string + - name: page + in: path + description: Page number + required: true + style: simple + explode: false + schema: + type: number + - name: table + in: path + description: Table number + required: true + style: simple + explode: false + schema: + type: number + responses: + 200: + description: Ok + content: + text/csv: + schema: + type: string + example: | + 3x4 table;Empty column;Numbers + ;; + Item A;;3.14 + "Item B + on two lines";;1,234.56 + 404: + description: Not Found + x-swagger-router-controller: Output +components: + schemas: + queueStatus: + type: object + properties: + progress-percentage: + type: number + example: 25 + status: + type: string + example: detecting reading order + start-date: + type: string + format: date-time + estimated-remaining-time: + type: number + example: 60 + config: + type: object + properties: + version: + type: number + extractor: + $ref: '#/components/schemas/config_extractor' + cleaner: + oneOf: + - type: string + - type: object + output: + $ref: '#/components/schemas/config_output' + example: + verison: '1.0' + element: + type: object + properties: + id: + type: number + xml: + attribute: true + type: + type: string + xml: + attribute: true + enum: + - character + - drawing + - heading + - image + - list + - paragraph + - table + - table-cell + - table-row + - word + - line + - barcode + box: + $ref: '#/components/schemas/box' + content: + type: array + items: + $ref: '#/components/schemas/element' + box: + type: object + properties: + l: + type: number + t: + type: number + w: + type: number + h: + type: number + json: + type: object + properties: + metadata: + type: array + items: + $ref: '#/components/schemas/json_metadata' + pages: + type: array + items: + $ref: '#/components/schemas/json_pages' + fonts: + type: array + items: + $ref: '#/components/schemas/json_fonts' + body: + type: object + properties: + file: + type: string + format: binary + config: + $ref: '#/components/schemas/config' + inline_response_201: + type: object + properties: + id: + type: string + json: + type: string + xml: + type: string + markdown: + type: string + text: + type: string + confidances: + type: string + json-compact: + type: string + csv: + type: string + example: '{"id":"{id}","json":"https://doctech.ael.li/api/v1/json/{id}","markdown":"https://doctech.ael.li/api/v1/markdown/{id}","csv":"https://doctech.ael.li/api/v1/csv/{id}"}' + config_extractor: + type: object + properties: + pdf: + type: string + enum: + - pdf2json + - tesseract + - abbyy + img: + type: string + enum: + - tesseract + - abbyy + language: + type: string + config_output_formats: + type: object + properties: + json: + type: boolean + json-compact: + type: boolean + text: + type: boolean + markdown: + type: boolean + xml: + type: boolean + confidances: + type: boolean + csv: + type: boolean + pandas: + type: boolean + pdf: + type: boolean + config_output: + type: object + properties: + granularity: + type: string + enum: + - word + - character + - line + - paragraph + formats: + $ref: '#/components/schemas/config_output_formats' + json_metadata: + type: object + properties: + id: + type: number + xml: + attribute: true + order: + type: number + json_pages: + type: object + properties: + pageNumber: + type: number + xml: + attribute: true + box: + $ref: '#/components/schemas/box' + elements: + type: array + items: + $ref: '#/components/schemas/element' + json_fonts: + type: object + properties: + id: + type: number + xml: + attribute: true + name: + type: string + size: + type: number + parameters: + id: + name: id + in: path + description: ID of the document + required: true + style: simple + explode: false + schema: + type: string diff --git a/demo/jupyter-notebook/Jupyter - API Access Demo.ipynb b/demo/jupyter-notebook/Jupyter - API Access Demo.ipynb new file mode 100644 index 00000000..3632d842 --- /dev/null +++ b/demo/jupyter-notebook/Jupyter - API Access Demo.ipynb @@ -0,0 +1,232 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Jupyter Demo : Parsr API Access" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This demo provides a demo showing how one can process a document (pdf or image) using the Parsr pipeline's API interface to generate various outputs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Module Import" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import parsr_api" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Send document for processing" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "p = parsr_api.ParserApi('localhost:3001')" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'file': './sampleFile.pdf',\n", + " 'config': './sampleConfig.json',\n", + " 'status_code': 202,\n", + " 'server_response': '03a5ec93d322dec99e2887b1245ca3'}" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "job = p.sendDocument('./sampleFile.pdf', './sampleConfig.json')\n", + "jobId = job['server_response']\n", + "job" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Query the queue for status" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'request_id': '03a5ec93d322dec99e2887b1245ca3',\n", + " 'server_response': '{\"id\":\"03a5ec93d322dec99e2887b1245ca3\",\"json\":\"/api/v1/json/03a5ec93d322dec99e2887b1245ca3\",\"csv\":\"/api/v1/csv/03a5ec93d322dec99e2887b1245ca3\",\"text\":\"/api/v1/text/03a5ec93d322dec99e2887b1245ca3\",\"markdown\":\"/api/v1/markdown/03a5ec93d322dec99e2887b1245ca3\"}'}" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p.getStatus(jobId)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get the Raw Text output" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "txt_output = p.getText(jobId)['server_response']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get the Markdown output" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "md_output = p.getMarkdown(jobId)['server_response']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Get the full JSON output" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "json_output = p.getJson(jobId)['server_response']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interpret the JSON output" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "import parsr_output_interpreter as p\n", + "pa = p.ParsrOutputInterpreter(json_output)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get all the text on Page 1" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Document Parsing\\n\\nA Document Parsing system\\n\\nOfficial Website (work in progress)\\n\\nhttps://axatechlab.github.io/AXA-AEL-pdfparser/\\n\\nAPI\\n\\nTo start the API server, just run:\\n\\nnpm run start:api\\n\\nThe documentation is here.\\n\\nBinary dependencies for Linux and Mac OS X\\n\\nWe use qpdf, mupdf-tools, imagemagick and pdf2json to do process pdf files,extract fonts and convert pdf to json structure. your machine prior to use docparser.\\n\\nYou must install this tools on\\n\\npacman -S qpdf mupdf-tools pdf2json imagemagickapt-get install \\n\\nOn OS X:\\n\\nqpdf pdf2json imagemagick\\n\\n# # \\n\\nbrew install \\n\\nTesseract\\n\\nqpdf mupdf-tools pdf2json \\n\\nhttps://github.com/tesseract-ocr/tesseract/\\n\\nimagemagick\\n\\nOnly used if you give an image to the pipeline.\\n\\nDuckling\\n\\nArch LinuxDebian based \\n\\nFollow this guide: https://github.com/facebook/duckling#duckling-\\n\\nDependencies (Windows)\\n\\nlinux \\n\\nWe recommand using Chocolatey to install dependencies. It makes things muchmore easier to manage.\\n\\n1\\n\\ndistro\\n\\n'" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "pa.getTexts(page_number=1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Get tables on the first page as pandas dataframes - TODO" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# pa.getTables(page_number=1)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/demo/jupyter-notebook/parsr_api.py b/demo/jupyter-notebook/parsr_api.py new file mode 100644 index 00000000..f63a7a0f --- /dev/null +++ b/demo/jupyter-notebook/parsr_api.py @@ -0,0 +1,81 @@ +# +# Copyright 2019 AXA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import requests +import base64 +import os +from glob import glob +from itertools import chain +from IPython.core.display import display, HTML +import magic +mime = magic.Magic(mime=True) + + +class ParserApi: + def __init__(self, server:str): + self.server = server + self.jobId = None + + def __supported_input_files(self) -> list: + return ['*.pdf', '*.jpg', '*.jpeg', '*.png', '*.tiff', '*.tif',] + + def sendDocument(self, file:str, config:str) -> dict: + packet = { + 'file': (file, open(file, 'rb'), 'application/pdf'), + 'config': (config, open(config, 'rb'), 'application/json'), + } + r = requests.post('http://'+self.server+'/api/v1/document', files=packet) + return {'file': file, 'config': config, 'status_code': r.status_code, 'server_response': r.text} + + def sendDocumentsFromFolder(self, folder:str, config:str) -> list: + responses = [] + os.chdir(folder) + files = [glob.glob(e) for e in self.__supported_input_files()] + files_flat = list(chain.from_iterable(files)) + for file in files_flat: + packet = { + 'file': (file, open(file, 'rb'), 'application/pdf'), + 'config': (config, open(config, 'rb'), 'application/json'), + } + r = requests.post('http://'+self.server+'/api/v1/document', files=packet) + responses.append({'file': file, 'config': config, 'status_code': r.status_code, 'server_response': r.text}) + return responses + + def getStatus(self, request_id): + r = requests.get('http://{}/api/v1/queue/{}'.format(self.server, request_id)) + return {'request_id': request_id, 'server_response': r.text} + + def getJson(self, request_id): + r = requests.get('http://{}/api/v1/json/{}'.format(self.server, request_id)) + return {'request_id': request_id, 'server_response': r.json()} + + def getMarkdown(self, request_id): + r = requests.get('http://{}/api/v1/markdown/{}'.format(self.server, request_id)) + return {'request_id': request_id, 'server_response': r.text} + + def getText(self, request_id): + r = requests.get('http://{}/api/v1/text/{}'.format(self.server, request_id)) + return {'request_id': request_id, 'server_response': r.text} + + def getCsv(self, request_id, page=None, table=None): + if page is None and table is None: + r = requests.get('http://{}/api/v1/csv/{}'.format(self.server, request_id)) + else: + r = requests.get('http://{}/api/v1/csv/{}/{}/{}'.format(self.server, request_id, page, table)) + return {'request_id': request_id, 'server_response': r.text} + + def displayMarkdownAsHTML(self, markdown_content): + display(HTML(markdown_content)) \ No newline at end of file diff --git a/demo/jupyter-notebook/parsr_output_interpreter.py b/demo/jupyter-notebook/parsr_output_interpreter.py new file mode 100644 index 00000000..6eb3b947 --- /dev/null +++ b/demo/jupyter-notebook/parsr_output_interpreter.py @@ -0,0 +1,85 @@ +# +# Copyright 2019 AXA +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +import logging +import pandas as pd +from io import StringIO + +class ParsrOutputInterpreter: + def __init__(self, object=None): + logging.basicConfig(level=logging.DEBUG, format='%(name)s - %(levelname)s - %(message)s') + self.object = None + if object is not None: + self.loadObject(object) + + def __getTextTypes(self): + return ['word', 'line', 'character', 'paragraph', 'heading'] + + def __getTextObjects(self, page_number=None): + texts = [] + if page_number is not None: + page = self.getPage(page_number) + if page is None: + logging.error("Cannot get text elements for the requested page; Page {} not found".format(page_number)) + return None + else: + for element in page['elements']: + if element['type'] in self.__getTextTypes(): + texts.append(element) + else: + for page in self.object['pages']: + for element in page['elements']: + if element['type'] in self.__getTextTypes(): + texts.append(element) + return texts + + def __textFromTextObject(self, text_object:dict) -> str: + result = "" + if text_object['type'] in ['paragraph', 'heading']: + for i in text_object['content']: + result += self.__textFromTextObject(i) + elif text_object['type'] in ['line']: + for i in text_object['content']: + result += self.__textFromTextObject(i) + elif text_object['type'] in ['word']: + if type(text_object['content']) is list: + for i in text_object['content']: + result += self.__textFromTextObject(i) + else: + result += text_object['content'] + elif text_object['type'] in ['character']: + result += text_object['content'] + return result + + def loadObject(self, object): + self.object = object + + def getPage(self, page_number): + for p in self.object['pages']: + if p['pageNumber'] == page_number: + return p + logging.error("Page {} not found".format(page_number)) + return None + + def getTexts(self, page_number:int=None) -> str: + final_text = "" + for textObj in self.__getTextObjects(page_number): + final_text += self.__textFromTextObject(textObj) + final_text += "\n\n" + return final_text + + def getDataframeFromCSVString(self, csv_string:str, seperator:str=';') -> pd.DataFrame: + return pd.read_csv(StringIO(csv_string), sep=seperator) diff --git a/demo/jupyter-notebook/sampleConfig.json b/demo/jupyter-notebook/sampleConfig.json new file mode 100644 index 00000000..9b27b70e --- /dev/null +++ b/demo/jupyter-notebook/sampleConfig.json @@ -0,0 +1,44 @@ +{ + "version": 0.5, + "extractor": { + "pdf": "pdf2json", + "img": "tesseract", + "language": ["eng", "fra"] + }, + "cleaner": [ + "out-of-page-removal", + "whitespace-removal", + "redundancy-detection", + "reading-order-detection", + "link-detection", + ["words-to-line", { "maximumSpaceBetweenWords": 100 }], + "lines-to-paragraph", + "heading-detection", + ["header-footer-detection", { "maxMarginPercentage": 15 }], + "hierarchy-detection", + ["regex-matcher", { + "queries": [ + { + "label": "Car", + "regex": "([A-Z]{2}\\-[\\d]{3}\\-[A-Z]{2})" + }, { + "label": "Age", + "regex": "(\\d+)[ -]*(ans|jarige)" + }, { + "label": "Percent", + "regex": "([\\-]?(\\d)+[\\.\\,]*(\\d)*)[ ]*(%|per|percent|pourcent|procent)" + }] + }] + ], + "output": { + "granularity": "word", + "includeMarginals": false, + "formats": { + "json": true, + "text": true, + "csv": true, + "markdown": true, + "pdf": false + } + } +} diff --git a/demo/jupyter-notebook/sampleFile.pdf b/demo/jupyter-notebook/sampleFile.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a954db992220782937e0c471cd2b78e5abcf6932 GIT binary patch literal 44003 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 { + cb(null, `${__dirname}/pipeline/upload/`); + }, + filename: (req, file, cb) => { + cb(null, file.originalname); + }, + }), +}); + +const localStorage = require('./localStorage'); +const storage = localStorage.from(`${__dirname}/storage.json`); +// localStorage.autoSave = true; + +const app = express(); + +// config module and the configuration file path +const configModule = require(`${__dirname}/../../dist/src/types/Config`); +const configFilePath = path.resolve(`${__dirname}/../../server/defaultConfig.json`); + +let defaultConfig; +if (fs.existsSync(configFilePath)) { + let configFile = fs.readFileSync(configFilePath, 'utf-8'); + defaultConfig = new configModule.Config(JSON.parse(configFile)); + var cp = require('child_process'); + + console.log('Config read successfully from', configFilePath); + console.log( + util.inspect(defaultConfig, { + colors: true, + breakLength: 10, + }), + ); +} else { + console.log('ERROR: The config file', configFilePath, 'does not exist. Specify a valid path.'); + return process.exit(1); +} + +app.use(express.static(`${__dirname}/public`)); +app.use('/static/', express.static(`${__dirname}/node_modules/dropzone/dist/`)); +app.use('/static/', express.static(`${__dirname}/node_modules/jquery/dist/`)); +app.use('/static/', express.static(`${__dirname}/node_modules/riot/`)); +app.use('/static/', express.static(`${__dirname}/node_modules/bootstrap/dist/`)); + +app.get('/status/:id', (req, res) => { + res.json(storage[req.params.id]); +}); + +app.get('/analysed/:id', (req, res) => { + fs.readFile( + `${__dirname}/pipeline/4_analysed/${storage[req.params.id].originalname}.txt.json`, + 'utf8', + (err, data) => { + if (err) { + console.log(err); + } + res.send(data); + }, + ); +}); + +app.get('/json', (req, res) => { + let files = fs.readdirSync(`${__dirname}/pipeline/output`); + + files = files + .filter(f => f[0] !== '.') + .filter(f => f.substring(f.length - 5).toLowerCase() === '.json'); + + res.json(files); +}); + +app.get('/json/:id', (req, res) => { + console.log(decodeURIComponent(req.params.id)); + let path = `${__dirname}/pipeline/output/${decodeURIComponent(req.params.id)}`; + if (!fs.existsSync(path)) { + res.sendStatus(404); + return; + } + + let file = JSON.parse(fs.readFileSync(path, 'utf8')); + res.json(file); +}); + +app.get('/extracted', (req, res) => { + let files = fs.readdirSync(`${__dirname}/pipeline/6_extracted`); + + let contract = []; + for (let i = 0; i < files.length; ++i) { + if (files[i][0] === '.') { + continue; + } + let data = fs.readFileSync(`${__dirname}/pipeline/6_extracted/${files[i]}`); + contract.push({ + name: files[i], + data: JSON.parse(data), + }); + } + + res.json(contract); +}); + +app.post('/upload', upload.single('file'), (req, res) => { + req.setTimeout(0x7fffffff); + const fileObjectData = req.file; + const baseName = fileObjectData.originalname.substring( + 0, + fileObjectData.originalname.lastIndexOf('.'), + ); + const configPath = `${os.tmpdir()}/${baseName}-config-${crypto + .randomBytes(15) + .toString('hex')}.json`; + console.log('BASENAME', baseName); + const documentId = baseName; + + fileObjectData.id = fileObjectData.filename; + storage[fileObjectData.filename] = fileObjectData; + + let config = null; + + if (req.body.config) { + try { + config = JSON.stringify(JSON.parse(req.body.config)); + fs.writeFileSync(configPath, config); + } catch (e) { + res.status(400).send(e); + } + } + + let fileType = filetype( + fs.readFileSync(`${__dirname}/pipeline/upload/${fileObjectData.filename}`), + ); + + if (!config) { + if ( + !fileType || + fileType.ext === 'pdf' || + fileType.ext === 'xml' || + fileType.mime.slice(0, 5) === 'image' + ) { + fs.writeFileSync(configPath, JSON.stringify(defaultConfig)); + } else { + res.status(400).send('Input file is neither a PDF nor an image'); + } + } + + processPDF(fileObjectData.filename, documentId, configPath, res); +}); + +function processPDF(pdfFile, documentId, configName, res) { + console.log('Processing ' + pdfFile); + + debugFlag = true ? 'pipeline' : ''; + process.env.NODE_DEBUG = debugFlag; + + let args = [ + `../../dist/bin/index.js`, + '--input-file', + `${__dirname}/pipeline/upload/${pdfFile}`, + '--output-folder', + `${__dirname}/pipeline/output`, + '--document-name', + documentId, + '--config', + path.resolve(configName), + '--pretty-logs', + ]; + + let extractor = spawn(`node`, args, { + env: process.env, + }); + + extractor.stdout.pipe(process.stdout); + extractor.stderr.pipe(process.stdout); + + // FIXME don't redirect if code !== 0 + extractor.on('exit', function(code) { + console.log('Child process exited with code ' + code.toString()); + res.json({ + filename: documentId + '.json', + }); + }); +} + +try { + fs.mkdirSync(`${__dirname}/pipeline`); +} catch (e) { + // noop: folder already exists +} + +try { + fs.mkdirSync(`${__dirname}/pipeline/output`); +} catch (e) { + // noop: folder already exists +} + +try { + fs.mkdirSync(`${__dirname}/pipeline/upload`); +} catch (e) { + // noop: folder already exists +} + +let port = process.argv[2] || 3000; +const server = app.listen(port, () => { + const host = server.address().address === '::' ? '[::]' : server.address().address; + const port = server.address().port; + console.log(`App listening at http://${host}:${port}`); +}); + +process.on('exit', function(code) { + return console.log(`Exitting with code ${code}`); +}); diff --git a/demo/web-viewer/localStorage.js b/demo/web-viewer/localStorage.js new file mode 100644 index 00000000..fafc20e1 --- /dev/null +++ b/demo/web-viewer/localStorage.js @@ -0,0 +1,57 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const fs = require('fs'); + +// local storage FTW +let LOCAL_STORAGE = {}; +let STORAGE_NAME = ''; + +let from = name => { + STORAGE_NAME = name; + if (fs.existsSync(name)) { + JSON.parse(fs.readFileSync(name)); + } + return LOCAL_STORAGE; +}; + +let DATA = { + from: from, + storage: LOCAL_STORAGE, + autoSave: false, + autoSaveCadence: 10000, +}; + +let save = () => { + if (STORAGE_NAME) { + fs.writeFile(STORAGE_NAME, JSON.stringify(LOCAL_STORAGE), 'utf8', err => { + if (err) { + console.log('STORAGE ERROR : ', err); + } + }); + } +}; + +let cadenceSave = () => { + if (DATA.autoSave) { + save(); + } + setTimeout(cadenceSave, DATA.autoSaveCadence); +}; + +DATA.save = save; + +module.exports = DATA; diff --git a/demo/web-viewer/package-lock.json b/demo/web-viewer/package-lock.json new file mode 100644 index 00000000..87a1cf3b --- /dev/null +++ b/demo/web-viewer/package-lock.json @@ -0,0 +1,2448 @@ +{ + "name": "web-viewer", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@axa/web-toolkit": { + "version": "2.0.0-alpha.3", + "resolved": "https://registry.npmjs.org/@axa/web-toolkit/-/web-toolkit-2.0.0-alpha.3.tgz", + "integrity": "sha1-EoB54k2Ge4rPZnN7p3WtTJgkTMk=", + "requires": { + "tether": "^1.4.0" + } + }, + "@types/acorn": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.5.tgz", + "integrity": "sha512-603sPiZ4GVRHPvn6vNgEAvJewKsy+zwRWYS2MeIMemgoAtcjlw2G3lALxrb9OPA17J28bkB71R33yXlQbUatCA==", + "requires": { + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==" + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" + }, + "acorn-dynamic-import": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz", + "integrity": "sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==", + "requires": { + "acorn": "^5.0.0" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } + }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async-each": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", + "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + } + }, + "bootstrap": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", + "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chokidar": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", + "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" + } + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "date-time": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-2.1.0.tgz", + "integrity": "sha512-/9+C44X7lot0IeiyfgJmETtRMhBidBYM2QFFIkGa0U1k+hSyY87Nw7PY3eDqpvCBm7I3WCSfPeZskW/YYq6m4g==", + "requires": { + "time-zone": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + } + }, + "dropzone": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.5.1.tgz", + "integrity": "sha512-3VduRWLxx9hbVr42QieQN25mx/I61/mRdUSuxAmDGdDqZIN8qtP7tcKMa3KfpJjuGjOJGYYUzzeq6eGDnkzesA==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "eslint-config-riot": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-riot/-/eslint-config-riot-1.0.0.tgz", + "integrity": "sha1-+9ZThpgLMPvNDhMF1MP7hhTvIRk=" + }, + "estree-walker": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz", + "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + } + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" + }, + "file-type": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz", + "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "optional": true + } + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "graceful-fs": { + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-reference": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.1.2.tgz", + "integrity": "sha512-Kn5g8c7XHKejFOpTf2QN9YjiHHKl5xRj+2uAZf9iM2//nkBNi/NNeB5JMoun28nEaUVHyPUzqzhfRlfAirEjXg==", + "requires": { + "@types/estree": "0.0.39" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "jquery": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", + "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "locate-character": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-2.0.5.tgz", + "integrity": "sha512-n2GmejDXtOPBAZdIiEFy5dJ5N38xBCXLNOtw2WpB9kGh6pnrEuKlwYI+Tkpofc4wDtVXHtoAOJaMRlYG/oYaxg==" + }, + "lodash.every": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz", + "integrity": "sha1-64mYS+vENkJ5uzrvu9HKGb+mxqc=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.maxby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.maxby/-/lodash.maxby-4.6.0.tgz", + "integrity": "sha1-CCJABo88eiJ6oAqDgOTzjPB4bj0=" + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz", + "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "optional": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "parse-ms": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-1.0.1.tgz", + "integrity": "sha1-VjRtR0nXjyNDDKDHE4UK75GqNh0=" + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "pretty-ms": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-3.2.0.tgz", + "integrity": "sha512-ZypexbfVUGTFxb0v+m1bUyy92DHe5SyYlnyY0msyms5zd3RwyvNgyxZZsXXgoyzlxjx5MiqtXUdhUfvQbe0A2Q==", + "requires": { + "parse-ms": "^1.0.0" + } + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "require-relative": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz", + "integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "riot": { + "version": "3.13.2", + "resolved": "https://registry.npmjs.org/riot/-/riot-3.13.2.tgz", + "integrity": "sha512-L4WFJEhbTA0deDoZ1XxoVw7Tf96+xYc06aQ+4QM+IkYClD6mZ7E/9zN3Rd48uYMBvHQfHtbPvR0KEiu3pJyI2A==", + "requires": { + "riot-cli": "^4.0.2", + "riot-compiler": "^3.5.2", + "riot-observable": "^3.0.0", + "riot-tmpl": "^3.0.8", + "simple-dom": "1.3.0", + "simple-html-tokenizer": "^0.5.7" + }, + "dependencies": { + "riot-cli": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/riot-cli/-/riot-cli-4.1.2.tgz", + "integrity": "sha512-JuRHDJKtGvAMksulzYHQRmOmzeICIOLe/PHvRuByfRlqGa0IP87baHATkKF4uwveMQKx3mq4JTvhfQHxilhi4g==", + "requires": { + "chalk": "^2.3.2", + "chokidar": "^2.0.3", + "co": "^4.6.0", + "optionator": "^0.8.2", + "riot-compiler": "^3.5.1", + "rollup": "^0.57.1" + } + } + } + }, + "riot-compiler": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/riot-compiler/-/riot-compiler-3.6.0.tgz", + "integrity": "sha512-P2MVj0T9ox0EFPTSSHJIAaBe6/fCnKln76BtPK6ezAlBW2+qKCDzzvkj3gwFvGEG1dYUHa2jQ/O6+FZNNe8CYw==", + "requires": { + "skip-regex": "^0.3.1", + "source-map": "^0.7.2", + "string-similarity": "^1.2.0" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" + } + } + }, + "riot-observable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/riot-observable/-/riot-observable-3.0.0.tgz", + "integrity": "sha1-i70tr3KiFBuwQwgt9AI9xQS60us=" + }, + "riot-tmpl": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/riot-tmpl/-/riot-tmpl-3.0.8.tgz", + "integrity": "sha1-3WVOcqOhUgywCcvvcMc4Vt7VhKY=", + "requires": { + "eslint-config-riot": "^1.0.0" + } + }, + "rollup": { + "version": "0.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.57.1.tgz", + "integrity": "sha512-I18GBqP0qJoJC1K1osYjreqA8VAKovxuI3I81RSk0Dmr4TgloI0tAULjZaox8OsJ+n7XRrhH6i0G2By/pj1LCA==", + "requires": { + "@types/acorn": "^4.0.3", + "acorn": "^5.5.3", + "acorn-dynamic-import": "^3.0.0", + "date-time": "^2.1.0", + "is-reference": "^1.1.0", + "locate-character": "^2.0.5", + "pretty-ms": "^3.1.0", + "require-relative": "^0.8.7", + "rollup-pluginutils": "^2.0.1", + "signal-exit": "^3.0.2", + "sourcemap-codec": "^1.4.1" + } + }, + "rollup-pluginutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.0.tgz", + "integrity": "sha512-8TomM64VQH6w+13lemFHX5sZYxLCxHhf9gzdRUEFNXH3Z+0CDYy7Grzqa6YUbZc0GIrfbWoD5GXZ3o5Teqh9ew==", + "requires": { + "estree-walker": "^0.6.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-dom": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/simple-dom/-/simple-dom-1.3.0.tgz", + "integrity": "sha512-RVjr6e80FFGDqDJZeQd4EMwoDLatn4Jy2SfuXecrP1IgG4ZAqkGSokE8LNV5i0kzWR2IM0e257xGN9JS8lxm0Q==" + }, + "simple-html-tokenizer": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.5.7.tgz", + "integrity": "sha512-APW9iYbkJ5cijjX4Ljhf3VG8SwYPUJT5gZrwci/wieMabQxWFiV5VwsrP5c6GMRvXKEQMGkAB1d9dvW66dTqpg==" + }, + "skip-regex": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/skip-regex/-/skip-regex-0.3.1.tgz", + "integrity": "sha1-F5GarirEzj1h1ed+7diCBsZKohU=" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "sourcemap-codec": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.4.tgz", + "integrity": "sha512-CYAPYdBu34781kLHkaW3m6b/uUSyMOC2R61gcYMWooeuaGtjof86ZA/8T+qVPPt7np1085CR9hmMGrySwEc8Xg==" + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-similarity": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.2.2.tgz", + "integrity": "sha512-IoHUjcw3Srl8nsPlW04U3qwWPk3oG2ffLM0tN853d/E/JlIvcmZmDY2Kz5HzKp4lEi2T7QD7Zuvjq/1rDw+XcQ==", + "requires": { + "lodash.every": "^4.6.0", + "lodash.flattendeep": "^4.4.0", + "lodash.foreach": "^4.5.0", + "lodash.map": "^4.6.0", + "lodash.maxby": "^4.6.0" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tether": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/tether/-/tether-1.4.6.tgz", + "integrity": "sha512-TyWPw9O0ExqH9/ZBnQ0P1/mNI6LX16YPx5XvixC/ZvAqMkhGeXmKTTsMbSBn3ViOrPuQi/Uef11bVp3sd5UcQQ==" + }, + "time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha1-mcW/VZWJZq9tBtg73zgA3IL67F0=" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "upath": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", + "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==" + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + } + } +} diff --git a/demo/web-viewer/package.json b/demo/web-viewer/package.json new file mode 100644 index 00000000..53fc73aa --- /dev/null +++ b/demo/web-viewer/package.json @@ -0,0 +1,24 @@ +{ + "name": "web-viewer", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build:sass": "node-sass style/style.scss public/css/compiled.css", + "build:sass:watch": "node-sass -w style/style.scss public/css/compiled.css", + "start": "npm install && npm run build:sass && node index.js" + }, + "author": "AXA rev", + "license": "Apache-2.0", + "dependencies": { + "@axa/web-toolkit": "^2.0.0-alpha.3", + "bootstrap": "^4.3.1", + "dropzone": "^5.5.1", + "express": "^4.17.1", + "file-type": "^11.1.0", + "jquery": "^3.4.1", + "multer": "^1.4.1", + "riot": "^3.13.2" + } +} diff --git a/demo/web-viewer/public/css/style.css b/demo/web-viewer/public/css/style.css new file mode 100644 index 00000000..cb47a410 --- /dev/null +++ b/demo/web-viewer/public/css/style.css @@ -0,0 +1,33 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +.clause { + margin: 16px; + padding: 16px; + border: 1px solid #eee; + border-radius: 2px; + border-left: 2px solid #888; +} + +.block { + margin: 16px; + padding: 16px; + border: 1px solid #eee; + border-radius: 2px; + border-left: 2px solid #888; + background-color: #666; + color: #eee; +} diff --git a/demo/web-viewer/public/css/viewer.css b/demo/web-viewer/public/css/viewer.css new file mode 100644 index 00000000..36a66083 --- /dev/null +++ b/demo/web-viewer/public/css/viewer.css @@ -0,0 +1,105 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +body { + background: floralwhite; +} + +#render { + transform: scale(1) translateX(-50%); + left: 50%; + transform-origin: 0 0; + position: relative; +} + +#folder { + position: fixed; + z-index: 1; + overflow: auto; + max-height: 100%; + opacity: 0.3; + transition: 150ms opacity; +} + +#folder:hover { + opacity: 1; + z-index: 10; +} + +.page { + z-index: 5; + background-color: white; + border: 1px solid black; + position: relative; + margin: 30px 0; + box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); + transform: translateX(-50%); + left: 50%; +} + +.textItem { + background-color: #eef; + overflow: hidden; + /* pointer-events: none; */ +} + +.selectionBox { + position: absolute; + border: 1px solid #0f0fff; + background-color: #0f0fff22; +} + +.tag-bullet-list { + background-color: #aaffaa; +} + +.tag-paragraph { + box-shadow: 3px 3px 0 #9999ff; +} + +.tag-redundancy { + background-color: #ff9999; +} + +.tag-title { + background-color: pink; +} + +.tag-definition { + background-color: orange; +} + +.tag-page-number { + background-color: rgb(0, 254, 255); +} + +.table-box { + background-color: #d3ffe3; +} + +.locale-box { + background-color: #fff; + border: 1px solid #ddd; + padding: 4px; +} + +.nerDimension { + display: inline-block; + color: #fff; + background-color: #444; + padding: 4px; + border-radius: 2px; +} diff --git a/demo/web-viewer/public/favicon.ico b/demo/web-viewer/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b3d3099beccb3718c7cc30092eaeacb4b3d059a7 GIT binary patch literal 1150 zcmbVM-%C?r82)y)(Zq&^bBbM9ckQOTet=jkOKe~kl!4JmyvQ4gfuXP@NRg)QxU-$@ z!eFB|u>xTijZwj5{gNR#7(s&;XUtppZ9&9?>yfJhcO-9 zt*wlnN7%T5F)L&27zIIu{KhFUM!ov$A!At9gh=l^yqc`-b6i_L861RUGAWp-*CS%L zBhu4@^^p-odVBGFco@q~O-eu5*@<&A(@yS)ur!x!7z@m7)$>+3_Hr3InxZhWJeTq1#TK9BVLJS4&c77MbgtN1{( zS`Xa+BbyB$lS#Z^S;1yFjJ;wJpVMjlESK?VV*{CJ6luR7KT0J$bvSDHp8b7#HZ+7c zvW%3+gDPB zvxEnNrADI>zN&G{bRCzP?_rS{tKZNlpKB~JJuWv-!)dzlUwrOkPB?@MW)XLcrxhOy s`~J9oBO%e;w82Ju;e%U$6mwK_4eq1SSftsT2d=<=Z~^uIgMY1m0d4f + + + Parsr + + + + + + + + + +
+
+
+ + + + + + + +
+
+
+
+ +
+

+ Warning! This platform is provided as a demo and should not be used with confidential documents. + Every document uploaded on this demo will be accessible by anybody and should thus be considered public. + We cannot be held responsible of any data leak through this platform. +

+
+
+
+ + + +
+
+

+ Once dropped, the document will be parsed and processed.
+ This operation can take a lot of time depending of the length of your document. +

+
+ + + + + + + diff --git a/demo/web-viewer/public/js/renderer.js b/demo/web-viewer/public/js/renderer.js new file mode 100644 index 00000000..a1e0f1a0 --- /dev/null +++ b/demo/web-viewer/public/js/renderer.js @@ -0,0 +1,346 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +class Renderer { + constructor(target) { + this.target = document.querySelector(target); + this.properties = []; + } + + loadPdf(filename) { + return fetch('/json/' + encodeURIComponent(filename)) + .then(response => { + if (!response.ok) { + throw new Error(`${response.status}`); + } else { + return response; + } + }) + .then(response => response.json()); + } + + textToString(text) { + if (!text.content) { + return ''; + } + if (typeof text.content === 'string') { + return text.content; + } + return text.content.reduce((a, b, index, arr) => { + return { content: this.textToString(a) + ' ' + this.textToString(b) }; + }, '').content; + } + + rescale() { + const maxPossibleWidth = 1000; + let maxWidth = 0; + + for (let page of document.querySelectorAll('.page')) { + maxWidth = Math.max(page.offsetWidth, maxWidth); + } + + const bodyWidth = document.querySelector('body').offsetWidth; + + if (bodyWidth < maxWidth || maxWidth > maxPossibleWidth) { + let scale = Math.min(bodyWidth / maxWidth, maxPossibleWidth / maxWidth); + document.querySelector('#render').style.transform = `scale(${scale}) translate(-50%)`; + this.target.style.height = this.currentHeight * scale + 'pt'; + } else { + document.querySelector('#render').style.transform = `scale(1) translate(-50%)`; + this.target.style.height = this.currentHeight + 'pt'; + } + } + + render(pdf) { + this.pdf = pdf; + this.styleDico = {}; + this.currentHeight = 0; + this.metadata = []; + console.log('pdf', pdf); + // this.createStyleDico(this.pdf.fontCatalog); + + pdf.metadata.forEach(m => { + this.metadata[m.id] = m; + }); + + for (let i = 0; i < this.pdf.pages.length; ++i) { + console.log('rendering page', i + 1); + + let page = this.pdf.pages[i]; + + let pageDiv = document.createElement('div'); + pageDiv.classList.add('page'); + pageDiv.style.height = page.box.h + 'pt'; + pageDiv.style.width = page.box.w + 'pt'; + pageDiv.title = `Page ${i + 1} / ${this.pdf.pages.length}`; + this.target.appendChild(pageDiv); + this.renderPage(page, pageDiv); + this.currentHeight += page.box.h; + } + + this.target.style.height = this.currentHeight + 'pt'; + + this.rescale(); + } + + addFont(fontname, filename) { + let newStyle = document.createElement('style'); + newStyle.appendChild( + document.createTextNode( + `@font-face {\ + font-family: '${fontname}';\ + src: url('/mutool-extraction/${filename}');\ + }`, + ) + ); + document.head.appendChild(newStyle); + } + + // TODO Handle other format than TrueType + createStyleDico(fontCatalog) { + for (let i = 0; i < fontCatalog.length; ++i) { + this.addFont(fontCatalog[i].family, fontCatalog[i].family + '.ttf'); + let font = fontCatalog[i]; + this.styleDico[font.fontspec] = { + font: `${font.style} ${font.weight} ${font.size}px ${font.family}`, + // color: font.color + }; + } + } + + renderPage(page, target) { + if (page.locale) { + let div = document.createElement('div'); + div.innerText = page.localeCode; + div.style.position = 'absolute'; + div.style.top = '0px'; + div.style.right = '-50px'; + div.className = 'locale-box'; + target.appendChild(div); + } + if (page.tables) { + for (let i = 0; i < page.tables.length; i++) { + let table = page.tables[i]; + let div = document.createElement('div'); + div.style.position = 'absolute'; + div.style.top = table.top + 'pt'; + div.style.left = table.left + 'pt'; + div.style.width = table.width + 'pt'; + div.style.height = table.height + 'pt'; + div.className = 'table-box'; + target.appendChild(div); + } + } + + for (let i = 0; i < page.elements.length; i++) { + this.renderElement(page.elements[i], target, 0, 0, 0, 1); + } + } + + renderElement(element, parent, leftRef, topRef, depth, ratio) { + let e = element; + e.metadata = e.metadata.map(id => this.metadata[id]); + + if (typeof e.box === 'undefined') { + for (let i = 0; i < e.content.length; i++) { + this.renderElement( + e.content[i], + parent, + parseFloat(parent.style.left) + leftRef, + parseFloat(parent.style.top) + topRef, + depth + 1, + ); + } + + return; + } + + // FIXME Remove me once Abbyy told us what's going on with the table ratios. + // if (e.type === 'table') { + // let ratio = 1; + // let W = e.content[0].box.w; + // + // if (e.box.w < W) { + // ratio = e.box.w / W; + // } + // + // e.box.w *= ratio; + // e.box.h *= ratio; + // + // let topAcc = 0; + // for (let i = 0; i < e.content.length; i++) { + // let row = e.content[i]; + // row.box.w *= ratio; + // row.box.h *= ratio; + // row.box.t -= topAcc; + // + // topAcc += row.box.h; + // + // let leftAcc = 0; + // for (let i = 0; i < row.content.length; i++) { + // let cell = row.content[i]; + // cell.box.t = row.box.t; + // cell.box.w *= ratio; + // cell.box.h *= ratio; + // cell.box.l -= leftAcc; + // + // leftAcc += cell.box.w; + // } + // } + // } + + // Wrap character in wrapper div + if (e.type === 'character') { + let wrapper = document.createElement('div'); + + wrapper.style.position = 'absolute'; + wrapper.style.left = e.box.l - leftRef + 'pt'; + leftRef = e.box.l; + wrapper.style.top = 0; + wrapper.style.width = e.box.w + 'pt'; + wrapper.style.height = '100%'; + wrapper.style.zIndex = 100 + depth; + wrapper.classList.add(`wrapper-${e.type}`); + wrapper.innerText = e.content; + e.content = ''; + + parent.appendChild(wrapper); + parent = wrapper; + } + + let div = document.createElement('div'); + + div.style.position = 'absolute'; + div.style.left = e.box.l - leftRef + 'pt'; + div.style.top = e.box.t - topRef + 'pt'; + div.style.width = e.box.w + 'pt'; + div.style.height = e.box.h + 'pt'; + div.style.zIndex = 100 + depth; + + if (e.type !== 'character') { + div.style.fontSize = e.box.h * 0.85 + 'pt'; // FIXME use proper font size + } + + div.classList.add(`element-${e.type}`); + + div.setAttribute('data-id', e.id); + div.setAttribute('data-box-l', e.box.l); + div.setAttribute('data-box-t', e.box.t); + div.setAttribute('data-box-w', e.box.w); + div.setAttribute('data-box-h', e.box.h); + + if (e.colspan) { + div.setAttribute('data-colspan', e.colspan); + } + + if (e.rowspan) { + div.setAttribute('data-rowspan', e.rowspan); + } + + if ( + typeof e.properties !== 'undefined' && + typeof e.properties.order !== 'undefined' + ) { + //div.style.backgroundColor = `rgb(${240-e.properties.order/4}, ${240-e.properties.order/2}, 255)`; + } + + if (typeof e.content === 'string') { + div.innerText = e.content; + div.setAttribute('data-text', e.content); + div.title = + (JSON.stringify(e.content).length > 40 ? '...' : e.content) + + '\n\n' + + (e.properties ? JSON.stringify(e.properties) : 'no properties') + + '\n\n' + + (e.metadata && e.metadata.length > 0 ? JSON.stringify(e.metadata) : 'no metadata'); + if (e.metadata && e.metadata.length > 0) { + div.style.backgroundColor = "red"; + } + } else if (Array.isArray(e.content)) { + for (let i = 0; i < e.content.length; i++) { + this.renderElement( + e.content[i], + div, + parseFloat(div.style.left) + leftRef, + parseFloat(div.style.top) + topRef, + depth + 1, + ratio, + ); + } + } else { + console.warn('Attribute content of', e, 'should not be empty'); + } + + parent.appendChild(div); + + // let ner = renderNer(e.content, e.properties); + + // div.innerText = ner.replace(/\n/g, '
'); + // div.style.font = this.styleDico[e.font].font; + // div.style.color = this.styleDico[e.font].color; + + // div.className = 'textItem ' + Array.from(e.properties, ([k, v]) => k).map((t) => 'tag-' + t).join(' '); + } +} + +if (!String.prototype.splice) { + /** + * {JSDoc} + * + * The splice() method changes the content of a string by removing a range of + * characters and/or adding new characters. + * + * @this {String} + * @param {number} start Index at which to start changing the string. + * @param {number} delCount An integer indicating the number of old chars to remove. + * @param {string} newSubStr The String that is spliced in. + * @return {string} A new string with the spliced substring. + */ + String.prototype.splice = function(start, delCount, newSubStr) { + return this.slice(0, start) + newSubStr + this.slice(start + Math.abs(delCount)); + }; +} + +function renderNer(text, properties) { + let indexMax = -1; + let cumul = 0; + for (let i = 0; i < properties.length; ++i) { + if (properties[i][0].indexOf('label') == 0) { + let data = properties[i][1]; + //["duration", "time"].indexOf(data.dim) != -1 + if ( + ['number', 'phone-number', 'url', 'temperature'].indexOf(data.dim) == -1 && + data.start > indexMax + ) { + let template = + "" + + data.body + + ''; + + text = text.splice(cumul + data.start, cumul + data.end - data.start, template); + + cumul += template.length; + indexMax = data.end + cumul; + } + } + } + + return text; +} diff --git a/demo/web-viewer/public/js/viewer.js b/demo/web-viewer/public/js/viewer.js new file mode 100644 index 00000000..d92f7308 --- /dev/null +++ b/demo/web-viewer/public/js/viewer.js @@ -0,0 +1,62 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +fetch('/json') + .then(response => response.json()) + .then(files => updateFolder(files)); + +let renderer = new Renderer('#render'); + +let file = undefined; +const spinnerTag = document.querySelector('#rendering-spinner'); +const folderTag = document.querySelector('#folder'); + +if (document.location.search) { + file = document.location.search.substring(1).match(/file=([^&]*)/)[1]; + spinnerTag.classList.add('is-active'); + folderTag.style.display = 'none'; + + renderer + .loadPdf(file) + .then(pdf => { + renderer.render(pdf); + spinnerTag.classList.remove('is-active'); + }) + .catch(err => { + folderTag.style.display = 'block'; + spinnerTag.classList.remove('is-active'); + console.error(err); + alert(err); + }); +} + +window.addEventListener('resize', () => { + renderer.rescale(); +}); + +function updateFolder(files) { + let folder = folderTag; + let str = ''; + str += '

Folder

'; + str += '

'; + str += files + .map(file => { + return `${file}
`; + }) + .join('\n'); + str += '

'; + folder.innerHTML = str; +} diff --git a/demo/web-viewer/public/viewer.html b/demo/web-viewer/public/viewer.html new file mode 100644 index 00000000..ef3b91cd --- /dev/null +++ b/demo/web-viewer/public/viewer.html @@ -0,0 +1,54 @@ + + + + Parsr + + + + +
+
+
+ + + + + + + +
+
+
+ +
+

Viewer

+
+
+
Rendering JSON Output...
+
+
+
+
+ + + + diff --git a/demo/web-viewer/public/views/downloader.tpl b/demo/web-viewer/public/views/downloader.tpl new file mode 100644 index 00000000..1a132c66 --- /dev/null +++ b/demo/web-viewer/public/views/downloader.tpl @@ -0,0 +1,30 @@ + +
+
+

Drag and drop a document

+

The document can be a PDF or an image.

+
+
+
+ + + + \ No newline at end of file diff --git a/demo/web-viewer/public/views/loader.tpl b/demo/web-viewer/public/views/loader.tpl new file mode 100644 index 00000000..0fcccd15 --- /dev/null +++ b/demo/web-viewer/public/views/loader.tpl @@ -0,0 +1,38 @@ + + +

Policy analysis

+
+
+ +
\ No newline at end of file diff --git a/demo/web-viewer/public/views/visualization.tpl b/demo/web-viewer/public/views/visualization.tpl new file mode 100644 index 00000000..0742f28b --- /dev/null +++ b/demo/web-viewer/public/views/visualization.tpl @@ -0,0 +1,47 @@ + + +

Policy Details

+ + +
+ +
\ No newline at end of file diff --git a/demo/web-viewer/style/style.scss b/demo/web-viewer/style/style.scss new file mode 100644 index 00000000..efd91497 --- /dev/null +++ b/demo/web-viewer/style/style.scss @@ -0,0 +1,164 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@import './node_modules/@axa/web-toolkit/scss/style'; + +$color-shy-tomato: #c91432; +$color-teal: #027180; +$color-deep-sapphire: #00005b; + +$color-igloo: #b5d0ee; +$color-cotton-candy: #fad6de; +$color-azalea: #f1afc6; +$color-aqua-green: #9fd9b4; +$color-logan: #9190ac; +$color-greyjoy: #9fbeaf; +$color-viridian-green: #668980; +$color-pacific: #00adc6; +$color-teal: #027180; +$color-acid: #f0ff93; +$color-dune: #fcd385; +$color-apache: #ddbe65; +$color-tosca: #914146; + +.disclamer { + background-color: $color-shy-tomato; + color: white; + width: 100%; + + p { + margin-top: 10px; + /* max-width: fit-content; */ + } +} + +.jumbotron { + color: $color-white; + + .dz-message { + color: $color-mineshaft; + font-size: 2em; + } +} + +#render { + transform: scale(1) translateX(-50%); + left: 50%; + transform-origin: 0 0; + position: relative; + line-height: 1; + white-space: nowrap; + + .page { + z-index: 5; + background-color: white; + border: 1px solid black; + position: relative; + margin: 30px 0; + box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.3); + transform: translateX(-50%); + left: 50%; + + .element-textItem { + background-color: #eff; + overflow: hidden; + user-select: none; + /* pointer-events: none; */ + } + + .element-paragraph { + background-color: #aaffaa; + } + + .element-heading { + background-color: #aaffaa; + } + + .element-line { + background-color: #9999ff; + } + + .element-word { + background-color: #ff9999; + } + + .element-character { + border: 1px solid red; + } + + .element-table { + background-color: orange; + } + + .element-table-row { + background-color: rgb(0, 254, 255); + border: 1px solid black; + } + + .element-table-cell { + background-color: #d3ffe3; + border: 1px solid black; + box-sizing: content-box; + } + + .tag-bullet-list { + background-color: $color-aqua-green; + } + + .tag-paragraph { + border: 1px solid $color-pacific; + } + + .tag-redundancy { + background-color: $color-logan; + } + + .tag-title { + background-color: $color-acid; + } + + .tag-definition { + background-color: orange; + } + + .tag-page-number { + background-color: $color-pacific; + } + + .table-box { + background-color: $color-indigo; + border: 1px solid $color-axa-blue; + } + + .locale-box { + background-color: $color-white; + border: 1px solid $color-mineshaft; + padding: 4px; + } + + .nerDimension { + display: inline-block; + color: $color-white; + background-color: $color-shy-tomato; + padding: 4px; + border-radius: 2px; + } + } +} + +.loading-spinner { + font-size: 1.8em; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..fe401c38 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.3' + +services: + duckling: + build: + context: docker/duckling + args: + DUCKLING_RELEASE: 'v0.1.6.1' + ports: + - 8000:8000 + image: duckling + + parsr: + build: + context: . + dockerfile: docker/parsr/Dockerfile + args: + DEV_MODE: 'true' + ports: + - 8080:3000 + - 3001:3001 + environment: + DUCKLING_HOST: http://duckling:8000 + ABBYY_SERVER_URL: + # NPM_RUN: start:all + image: parsr + volumes: + - ./pipeline/:/opt/app-root/src/demo/web-viewer/pipeline/ + +volumes: + pipeline: + driver: local diff --git a/docker/duckling/Dockerfile b/docker/duckling/Dockerfile new file mode 100644 index 00000000..2cc5bb39 --- /dev/null +++ b/docker/duckling/Dockerfile @@ -0,0 +1,24 @@ +FROM haskell:8 + +RUN mkdir /duckling /log && chown 1001:0 /duckling /log + + +RUN apt-get update && \ + apt-get install -qq -y libpcre3 libpcre3-dev build-essential --fix-missing --no-install-recommends + + +USER 1001 +ENV HOME /duckling + +ARG DUCKLING_RELEASE=master +RUN git clone --branch=${DUCKLING_RELEASE} https://github.com/facebook/duckling.git + +WORKDIR /duckling +RUN stack setup +# NOTE:`stack build` will use as many cores as are available to build +# in parallel. However, this can cause OOM issues as the linking step +# in GHC can be expensive. If the build fails, try specifying the +# '-j1' flag to force the build to run sequentially. +RUN stack build -j2 + +ENTRYPOINT stack exec duckling-example-exe diff --git a/docker/parsr/Dockerfile b/docker/parsr/Dockerfile new file mode 100644 index 00000000..2348910a --- /dev/null +++ b/docker/parsr/Dockerfile @@ -0,0 +1,77 @@ +FROM centos:7 AS builder +USER root + +RUN yum -y update && \ + yum-config-manager --enable epel && \ + yum -y groupinstall 'Development Tools' && \ + yum -y install git zlib-devel libjpeg-turbo-devel libtiff-devel libpng-devel && \ + mkdir /src && \ + cd /src + +RUN git clone https://github.com/AXATechLab/pdf2json && \ + cd pdf2json && \ + ./configure --prefix=/opt/app-root && \ + make -j && \ + make install && \ + cd /src && \ + rm -rf pdf2json + +RUN curl -o mupdf.tar.gz https://www.mupdf.com/downloads/archive/mupdf-1.14.0-source.tar.gz && \ + tar xvfz mupdf.tar.gz && \ + cd mupdf-1.14.0-source/ && \ + make prefix=/opt/app-root HAVE_GLUT=no -j install && \ + cd /src && \ + rm -rf mupdf-1.14.0-source + + +RUN curl -sL https://github.com/qpdf/qpdf/releases/download/release-qpdf-8.3.0/qpdf-8.3.0.tar.gz | tar xfz - && \ + cd qpdf-8.3.0 && \ + ./configure --prefix=/opt/app-root && \ + make -j && \ + make install && \ + cd /src && \ + rm -rf qpdf-8.3.0 + +RUN curl -sL https://github.com/jgm/pandoc/releases/download/2.7.3/pandoc-2.7.3-linux.tar.gz | tar xvfz - --strip-components 1 -C /opt/app-root + +RUN mkdir -p /opt/app-root/share/tessdata/ && \ + git clone https://github.com/tesseract-ocr/tessdata_fast.git && \ + cd tessdata_fast && \ + cp *.traineddata /opt/app-root/share/tessdata/ && \ + cd /src && \ + rm -rf tessdata_fast + +USER 1001 + + +FROM centos/nodejs-8-centos7 as engine +USER root + +RUN yum -y update && \ + yum -y install zlib libjpeg-turbo libtiff libpng ImageMagick + +RUN yum-config-manager --add-repo https://download.opensuse.org/repositories/home:/Alexander_Pozdnyakov/CentOS_7/ && \ + rpm --import https://build.opensuse.org/projects/home:Alexander_Pozdnyakov/public_key && \ + yum update && \ + yum -y install tesseract tesseract-langpack-* + +COPY --from=builder /opt/app-root/bin /opt/app-root/bin +COPY --from=builder /opt/app-root/etc /opt/app-root/etc +COPY --from=builder /opt/app-root/include /opt/app-root/include +COPY --from=builder /opt/app-root/lib /opt/app-root/lib +COPY --from=builder /opt/app-root/share /opt/app-root/share + +ENV PATH=$PATH:/opt/app-root/bin +ARG DEV_MODE=true + +# Copying in override assemble/run scripts +COPY .s2i/bin /tmp/scripts +COPY --chown=1001:root . /tmp/src + +USER 1001 + +RUN /tmp/scripts/assemble && \ + mkdir -p /opt/app-root/src/demo/web-viewer/pipeline/output && \ + chmod -R g+w /opt/app-root/src/demo/web-viewer && \ + rm -rf /tmp/src +CMD /tmp/scripts/run diff --git a/docs/API-deprecated.md b/docs/API-deprecated.md new file mode 100644 index 00000000..4f206e49 --- /dev/null +++ b/docs/API-deprecated.md @@ -0,0 +1,102 @@ +# API - DEPRECATED + +## :warning: This API is now deprecated. Please refer to the new version: [api-guide.md](docs/api-guide.md). :warning: + +The version of this API is 0.3. + +## Upload a file + +To upload a file, simply do an HTTP request of the following format: + +```http +URL: /upload +Method: POST +Form Data: + file: Document File + config: Json config +``` + +_The JSON config format is described below._ + +In the end, the full request may look like this: + +```http +POST /upload HTTP/1.1 +Host: localhost:3000 +Content-Type: multipart/form-data; boundary=MultipartBoundry + +--MultipartBoundry +Content-Disposition: form-data; name="config" +Content-Type: application/json + +{ + "version": 0.1, + "extractor": { + "fast-extractor": false, + "extract-tables": true + }, + "cleaner": [ + "fontMerge", + "removeOutOfPage", + "removeWhitespace", { "minWidth": 30 }, + "titleDetection", + "linkDetection", + "flagTableElements", + "textOrderDetection", + "lineMerge", { "maximumSpaceBetweenWords": 2, "mergeTableElements": true }, + "lineMerge", { "maximumSpaceBetweenWords": 10 }, + "redundancyDetection", + "removeWhitespace", + "paragraphMerge", + "paragraphLastLine" + ] +} +--MultipartBoundry-- +Content-Disposition: form-data; name="file"; filename="example.pdf" +Content-Type: application/pdf + + +--MultipartBoundry +``` + +## Response + +The response will be of the following format: + +```json +{ + "filename": "example.json" +} +``` + +## Get the JSON result + +Query: + +```http +URL: /json/example.json +Method: GET +``` + +## Response + +The response will be a JSON representing the document. Its format is derived from a [PdfFile.ts](https://github.com/AXATechLab/Parsr/blob/master/scripts/extractor/types/PdfFile.ts). + +It basically looks like this: + +```js +{ + "pages": [{ + "top": 0, + "left": 0, + "number": 1, + "pages": 28, + "height": 842, + "width": 595, + "tables": [...], + "paths": [...], + "text": [...] + }], + "fontCatalog": [...] +} +``` diff --git a/docs/api-guide.md b/docs/api-guide.md new file mode 100644 index 00000000..7a87e50a --- /dev/null +++ b/docs/api-guide.md @@ -0,0 +1,216 @@ +# API Guide + +This page is a guide on how to use the API. + +- [API Guide](#API-Guide) + - [0. Introduction](#0-Introduction) + - [1. Send Your Document: POST /document](#1-Send-Your-Document-POST-document) + - [`curl` command](#curl-command) + - [Status: 202 - Accepted](#Status-202---Accepted) + - [Status: 415 - Unsupported Media Type](#Status-415---Unsupported-Media-Type) + - [2. Get the queue status: GET /queue/{id}](#2-Get-the-queue-status-GET-queueid) + - [`curl` command](#curl-command-1) + - [Status: 200 - OK](#Status-200---OK) + - [Status: 201 - Created](#Status-201---Created) + - [Status: 404 - Not Found](#Status-404---Not-Found) + - [Status: 500 - Internal Server Error](#Status-500---Internal-Server-Error) + - [3. Get the results](#3-Get-the-results) + - [3.1. JSON, Markdown and Text results](#31-JSON-Markdown-and-Text-results) + - [`curl` command](#curl-command-2) + - [Status: 200 - OK](#Status-200---OK-1) + - [Status: 404 - Not Found](#Status-404---Not-Found-1) + - [3.2. CSV List of Files: GET /csv/{id}](#32-CSV-List-of-Files-GET-csvid) + - [`curl` command](#curl-command-3) + - [Status: 200 - OK](#Status-200---OK-2) + - [Status: 404 - Not Found](#Status-404---Not-Found-2) + - [3.3. CSV File: GET /csv/{id}/{page}/{table}](#33-CSV-File-GET-csvidpagetable) + - [`curl` command](#curl-command-4) + - [Status: 200 - OK](#Status-200---OK-3) + - [Status: 404 - Not Found](#Status-404---Not-Found-3) + +## 0. Introduction + +First of all there is a few things to know: + +- **The API is RESTful:** The API is over HTTP and follow REST standards. +- **The API is asynchronous:** There is a simple queue system and every job is managed by the API server. + +The API has an endpoint prefix `/api` and then, optionaly, the version number `/v1.0`. That mean every request must be send to: + +- `/api/v1.0`: will use the API version 1.0 +- `/api/v1`: will use the latest API version 1.x +- `/api`: will use the latest API version + +## 1. Send Your Document: [POST /document](https://axatechlab.github.io/Parsr/docs/api.html#api-Input-postDocument) + +First of all, you need to do a POST request to send the document to Parsr. Along that, you need to send the configuration to tell Parsr what kind of processing it must perform on the file. + +**Regarding the configuration file, please refer to the [configuration file documentation](configuration-file.md).** + +### `curl` command + +```sh +curl -X POST \ + http://localhost:3001/api/v1/document \ + -H 'Content-Type: multipart/form-data' \ + -F file='@/path/to/file.pdf;type=application/pdf' \ + -F config='@/path/to/config.json;type=application/json' +``` + +### Status: 202 - Accepted + +``` +00cafe4463b9c12aac145b3ee8f00d +``` + +The document you sent has been accepted and is being processed. The body contains the unique **queue ID**. You need to keep it somewhere for later, to know what's the queue status and get the results. + +### Status: 415 - Unsupported Media Type + +This error means the file format you sent is not supported by the platform (it's probably not a PDF or an Image). + +## 2. Get the queue status: [GET /queue/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Processing-getQueueStatus) + +This request allows you to get the status of the queued document being processed. You need to give it the **queue ID** that was return in the previous request. + +### `curl` command + +```sh +curl -X GET \ + http://localhost:3001/api/v1/queue/00cafe4463b9c12aac145b3ee8f00d +``` + +### Status: 200 - OK + +```json +{ + "estimated-remaining-time": 30, + "progress-percentage": 10, + "start-date": "2018-12-31T12:34:56.789Z", + "status": "Detecting reading order..." +} +``` + +This status means the document is still being processed. + +The `estimated-remaining-time` is expressed in seconds. + +_**NB:** `estimated-remaining-time` and `progress-percentage` are not working yet and are here and are placeholder for future usage._ + +### Status: 201 - Created + +```json +{ + "id": "00cafe4463b9c12aac145b3ee8f00d", + "json": "/api/v1/json/00cafe4463b9c12aac145b3ee8f00d", + "csv": "/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d", + "text": "/api/v1/text/00cafe4463b9c12aac145b3ee8f00d", + "markdown": "/api/v1/markdown/00cafe4463b9c12aac145b3ee8f00d" +} +``` + +This status is sent when the processing is done. It returns links to the generated resources and the ID of the document for convenience. + +### Status: 404 - Not Found + +This error means the queue ID doesn't refer to any known processing queue. + +### Status: 500 - Internal Server Error + +This error means that something went terribly wrong on the backend, probably an error comming Parsr. + +## 3. Get the results + +You can have results in different formats: +- JSON: [GET /json/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getJson) +- Markdown [GET /markdown/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getMarkdown) +- Raw text [GET /text/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getText) +- CSV [GET /csv/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getCsvList) + +These requests allow you to get the results of the processed document. You need to give it the **queue ID** that was return in a previous request. + +### 3.1. JSON, Markdown and Text results + +The queries for JSON, Markdown and raw text are all working in the same way. CSV is a bit different and is described in the next section. + +#### `curl` command + +```sh +curl -X GET \ + http://localhost:3001/api/v1/json/00cafe4463b9c12aac145b3ee8f00d +``` + +#### Status: 200 - OK + +```js +{ + "metadata": [/* ... */], + "fonts": [/* ... */], + "pages": [/* ... */], +} +``` + +For more information on the JSON format, please [refer to the specific guide](json-output.md). + +#### Status: 404 - Not Found + +This error means that the result file doesn't exist. Maybe it wasn't asked to be outputed in the config you sent in the first request. + +### 3.2. CSV List of Files: [GET /csv/{id}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getCsvList) + +Since you can have multiple tables per page, you need to query them in two steps: + +First of all, get the list of every CSV files' paths: + +#### `curl` command + +```sh +curl -X GET \ + http://localhost:3001/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d +``` + +#### Status: 200 - OK + +```json +[ + "/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d/1/1", + "/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d/2/1", + "/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d/2/2", + "/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d/3/1", +] +``` + +#### Status: 404 - Not Found + +This error means that the result file doesn't exist. Maybe it wasn't asked to be outputed in the config you sent in the first request. + +### 3.3. CSV File: [GET /csv/{id}/{page}/{table}](https://axatechlab.github.io/Parsr/docs/api.html#api-Output-getCsv) + +Then, we can get the CSV files one by one with the following parameters: + +- `{id}` is the ID of the document +- `{page}` is the page number +- `{table}` is the table number + +#### `curl` command + +```sh +curl -X GET \ + http://localhost:3001/api/v1/csv/00cafe4463b9c12aac145b3ee8f00d/1/1 +``` + +#### Status: 200 - OK + +```csv +3x4 table;Empty column;Numbers +;; +Item A;;3.14 +"Item B +on two lines";;1,234.56 +``` + +This CSV output example contains multiline cells and an empty column. + +#### Status: 404 - Not Found + +This error means that the result file doesn't exist. Maybe `{page}` and `{table}` parameters doesn't refer to an or it wasn't asked to be outputed in the config you sent in the first request. diff --git a/docs/api.html b/docs/api.html new file mode 100755 index 00000000..17ab79d3 --- /dev/null +++ b/docs/api.html @@ -0,0 +1,5076 @@ + + + + + Parsr API + + + + + + + + + + + + + +
+
+ +
+
+
+

Parsr API

+
+
+
+ +
+
+

Input

+
+
+
+

postDocument

+

Pipeline Input

+
+
+
+

+

Entry point to add a file to the processing queue.

+

+
+
/document
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X POST "https://localhost:3001/api/v1/document"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.InputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class InputApiExample {
+
+    public static void main(String[] args) {
+        
+        InputApi apiInstance = new InputApi();
+        byte[] file = file_example; // byte[] | 
+        config config = ; // config | 
+        try {
+            'String' result = apiInstance.postDocument(file, config);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling InputApi#postDocument");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.InputApi;
+
+public class InputApiExample {
+
+    public static void main(String[] args) {
+        InputApi apiInstance = new InputApi();
+        byte[] file = file_example; // byte[] | 
+        config config = ; // config | 
+        try {
+            'String' result = apiInstance.postDocument(file, config);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling InputApi#postDocument");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
byte[] *file = file_example; //  (optional)
+config *config = ; //  (optional)
+
+InputApi *apiInstance = [[InputApi alloc] init];
+
+// Pipeline Input
+[apiInstance postDocumentWith:file
+    config:config
+              completionHandler: ^('String' output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.InputApi()
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.postDocument(, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class postDocumentExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new InputApi();
+            var file = file_example;  // byte[] |  (optional) 
+            var config = new config(); // config |  (optional) 
+
+            try
+            {
+                // Pipeline Input
+                'String' result = apiInstance.postDocument(file, config);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling InputApi.postDocument: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiInputApi();
+$file = file_example; // byte[] | 
+$config = ; // config | 
+
+try {
+    $result = $api_instance->postDocument($file, $config);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling InputApi->postDocument: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::InputApi;
+
+my $api_instance = WWW::SwaggerClient::InputApi->new();
+my $file = file_example; # byte[] | 
+my $config = ; # config | 
+
+eval { 
+    my $result = $api_instance->postDocument(file => $file, config => $config);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling InputApi->postDocument: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.InputApi()
+file = file_example # byte[] |  (optional)
+config =  # config |  (optional)
+
+try: 
+    # Pipeline Input
+    api_response = api_instance.post_document(file=file, config=config)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling InputApi->postDocument: %s\n" % e)
+
+
+ +

Parameters

+ + + + +
Form parameters
+ + + + + + + + + + + +
NameDescription
file + + +
+
+
+ + byte[] + + + (binary) + + +
+
+
+
config + + +
+
+
+ + config + + +
+
+
+
+ + +

Responses

+

Status: 202 - Accepted

+ + + +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + +
NameTypeFormatDescription
LocationString
+
+
+ +

Status: 415 - Unsupported Media Type

+ + + +
+
+ +
+
+
+
+
+

Output

+
+
+
+

getCsv

+

Get the CSV representation of a table

+
+
+
+

+

+

+
+
/csv/{id}/{page}/{table}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/csv/{id}/{page}/{table}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.OutputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        BigDecimal page = 1.2; // BigDecimal | Page number
+        BigDecimal table = 1.2; // BigDecimal | Table number
+        try {
+            'String' result = apiInstance.getCsv(id, page, table);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getCsv");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.OutputApi;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        BigDecimal page = 1.2; // BigDecimal | Page number
+        BigDecimal table = 1.2; // BigDecimal | Table number
+        try {
+            'String' result = apiInstance.getCsv(id, page, table);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getCsv");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+BigDecimal *page = 1.2; // Page number
+BigDecimal *table = 1.2; // Table number
+
+OutputApi *apiInstance = [[OutputApi alloc] init];
+
+// Get the CSV representation of a table
+[apiInstance getCsvWith:id
+    page:page
+    table:table
+              completionHandler: ^('String' output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.OutputApi()
+var id = id_example; // {{String}} ID of the document
+var page = 1.2; // {{BigDecimal}} Page number
+var table = 1.2; // {{BigDecimal}} Table number
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getCsv(id, page, table, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getCsvExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new OutputApi();
+            var id = id_example;  // String | ID of the document
+            var page = 1.2;  // BigDecimal | Page number
+            var table = 1.2;  // BigDecimal | Table number
+
+            try
+            {
+                // Get the CSV representation of a table
+                'String' result = apiInstance.getCsv(id, page, table);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling OutputApi.getCsv: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiOutputApi();
+$id = id_example; // String | ID of the document
+$page = 1.2; // BigDecimal | Page number
+$table = 1.2; // BigDecimal | Table number
+
+try {
+    $result = $api_instance->getCsv($id, $page, $table);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling OutputApi->getCsv: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::OutputApi;
+
+my $api_instance = WWW::SwaggerClient::OutputApi->new();
+my $id = id_example; # String | ID of the document
+my $page = 1.2; # BigDecimal | Page number
+my $table = 1.2; # BigDecimal | Table number
+
+eval { 
+    my $result = $api_instance->getCsv(id => $id, page => $page, table => $table);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling OutputApi->getCsv: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.OutputApi()
+id = id_example # String | ID of the document
+page = 1.2 # BigDecimal | Page number
+table = 1.2 # BigDecimal | Table number
+
+try: 
+    # Get the CSV representation of a table
+    api_response = api_instance.get_csv(id, page, table)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling OutputApi->getCsv: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
page* + + +
+
+
+ + BigDecimal + + +
+ Page number +
+
+
+ Required +
+
+
+
table* + + +
+
+
+ + BigDecimal + + +
+ Table number +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok

+ + + +
+
+
+ +
+ +
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +
+
+
+
+
+
+

getCsvList

+

Get the list of every CSV file path

+
+
+
+

+

+

+
+
/csv/{id}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/csv/{id}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.OutputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            array['String'] result = apiInstance.getCsvList(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getCsvList");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.OutputApi;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            array['String'] result = apiInstance.getCsvList(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getCsvList");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+
+OutputApi *apiInstance = [[OutputApi alloc] init];
+
+// Get the list of every CSV file path
+[apiInstance getCsvListWith:id
+              completionHandler: ^(array['String'] output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.OutputApi()
+var id = id_example; // {{String}} ID of the document
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getCsvList(id, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getCsvListExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new OutputApi();
+            var id = id_example;  // String | ID of the document
+
+            try
+            {
+                // Get the list of every CSV file path
+                array['String'] result = apiInstance.getCsvList(id);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling OutputApi.getCsvList: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiOutputApi();
+$id = id_example; // String | ID of the document
+
+try {
+    $result = $api_instance->getCsvList($id);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling OutputApi->getCsvList: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::OutputApi;
+
+my $api_instance = WWW::SwaggerClient::OutputApi->new();
+my $id = id_example; # String | ID of the document
+
+eval { 
+    my $result = $api_instance->getCsvList(id => $id);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling OutputApi->getCsvList: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.OutputApi()
+id = id_example # String | ID of the document
+
+try: 
+    # Get the list of every CSV file path
+    api_response = api_instance.get_csv_list(id)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling OutputApi->getCsvList: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok

+ + + +
+
+
+ +
+ +
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +
+
+
+
+
+
+

getJson

+

Get the JSON representation of the document

+
+
+
+

+

+

+
+
/json/{id}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/json/{id}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.OutputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            json result = apiInstance.getJson(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getJson");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.OutputApi;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            json result = apiInstance.getJson(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getJson");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+
+OutputApi *apiInstance = [[OutputApi alloc] init];
+
+// Get the JSON representation of the document
+[apiInstance getJsonWith:id
+              completionHandler: ^(json output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.OutputApi()
+var id = id_example; // {{String}} ID of the document
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getJson(id, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getJsonExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new OutputApi();
+            var id = id_example;  // String | ID of the document
+
+            try
+            {
+                // Get the JSON representation of the document
+                json result = apiInstance.getJson(id);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling OutputApi.getJson: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiOutputApi();
+$id = id_example; // String | ID of the document
+
+try {
+    $result = $api_instance->getJson($id);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling OutputApi->getJson: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::OutputApi;
+
+my $api_instance = WWW::SwaggerClient::OutputApi->new();
+my $id = id_example; # String | ID of the document
+
+eval { 
+    my $result = $api_instance->getJson(id => $id);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling OutputApi->getJson: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.OutputApi()
+id = id_example # String | ID of the document
+
+try: 
+    # Get the JSON representation of the document
+    api_response = api_instance.get_json(id)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling OutputApi->getJson: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok

+ + + +
+
+
+ +
+ +
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +
+
+
+
+
+
+

getMarkdown

+

Get the Markdown representation of the document

+
+
+
+

+

+

+
+
/markdown/{id}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/markdown/{id}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.OutputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            'String' result = apiInstance.getMarkdown(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getMarkdown");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.OutputApi;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            'String' result = apiInstance.getMarkdown(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getMarkdown");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+
+OutputApi *apiInstance = [[OutputApi alloc] init];
+
+// Get the Markdown representation of the document
+[apiInstance getMarkdownWith:id
+              completionHandler: ^('String' output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.OutputApi()
+var id = id_example; // {{String}} ID of the document
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getMarkdown(id, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getMarkdownExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new OutputApi();
+            var id = id_example;  // String | ID of the document
+
+            try
+            {
+                // Get the Markdown representation of the document
+                'String' result = apiInstance.getMarkdown(id);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling OutputApi.getMarkdown: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiOutputApi();
+$id = id_example; // String | ID of the document
+
+try {
+    $result = $api_instance->getMarkdown($id);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling OutputApi->getMarkdown: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::OutputApi;
+
+my $api_instance = WWW::SwaggerClient::OutputApi->new();
+my $id = id_example; # String | ID of the document
+
+eval { 
+    my $result = $api_instance->getMarkdown(id => $id);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling OutputApi->getMarkdown: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.OutputApi()
+id = id_example # String | ID of the document
+
+try: 
+    # Get the Markdown representation of the document
+    api_response = api_instance.get_markdown(id)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling OutputApi->getMarkdown: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok

+ + + +
+
+
+ +
+ +
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +
+
+
+
+
+
+

getText

+

Get the raw text representation of the document

+
+
+
+

+

+

+
+
/text/{id}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/text/{id}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.OutputApi;
+
+import java.io.File;
+import java.util.*;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            'String' result = apiInstance.getText(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getText");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.OutputApi;
+
+public class OutputApiExample {
+
+    public static void main(String[] args) {
+        OutputApi apiInstance = new OutputApi();
+        String id = id_example; // String | ID of the document
+        try {
+            'String' result = apiInstance.getText(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling OutputApi#getText");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+
+OutputApi *apiInstance = [[OutputApi alloc] init];
+
+// Get the raw text representation of the document
+[apiInstance getTextWith:id
+              completionHandler: ^('String' output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.OutputApi()
+var id = id_example; // {{String}} ID of the document
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getText(id, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getTextExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new OutputApi();
+            var id = id_example;  // String | ID of the document
+
+            try
+            {
+                // Get the raw text representation of the document
+                'String' result = apiInstance.getText(id);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling OutputApi.getText: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiOutputApi();
+$id = id_example; // String | ID of the document
+
+try {
+    $result = $api_instance->getText($id);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling OutputApi->getText: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::OutputApi;
+
+my $api_instance = WWW::SwaggerClient::OutputApi->new();
+my $id = id_example; # String | ID of the document
+
+eval { 
+    my $result = $api_instance->getText(id => $id);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling OutputApi->getText: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.OutputApi()
+id = id_example # String | ID of the document
+
+try: 
+    # Get the raw text representation of the document
+    api_response = api_instance.get_text(id)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling OutputApi->getText: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok

+ + + +
+
+
+ +
+ +
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +
+
+
+
+
+

Processing

+
+
+
+

getQueueStatus

+

Get the status of the queue

+
+
+
+

+

Get the status of the queue

+

+
+
/queue/{id}
+

+

Usage and SDK Samples

+

+ + +
+
+
curl -X GET "https://localhost:3001/api/v1/queue/{id}"
+
+
+
import io.swagger.client.*;
+import io.swagger.client.auth.*;
+import io.swagger.client.model.*;
+import io.swagger.client.api.ProcessingApi;
+
+import java.io.File;
+import java.util.*;
+
+public class ProcessingApiExample {
+
+    public static void main(String[] args) {
+        
+        ProcessingApi apiInstance = new ProcessingApi();
+        String id = id_example; // String | ID of the document
+        try {
+            queueStatus result = apiInstance.getQueueStatus(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling ProcessingApi#getQueueStatus");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
import io.swagger.client.api.ProcessingApi;
+
+public class ProcessingApiExample {
+
+    public static void main(String[] args) {
+        ProcessingApi apiInstance = new ProcessingApi();
+        String id = id_example; // String | ID of the document
+        try {
+            queueStatus result = apiInstance.getQueueStatus(id);
+            System.out.println(result);
+        } catch (ApiException e) {
+            System.err.println("Exception when calling ProcessingApi#getQueueStatus");
+            e.printStackTrace();
+        }
+    }
+}
+
+ +
+
String *id = id_example; // ID of the document
+
+ProcessingApi *apiInstance = [[ProcessingApi alloc] init];
+
+// Get the status of the queue
+[apiInstance getQueueStatusWith:id
+              completionHandler: ^(queueStatus output, NSError* error) {
+                            if (output) {
+                                NSLog(@"%@", output);
+                            }
+                            if (error) {
+                                NSLog(@"Error: %@", error);
+                            }
+                        }];
+
+
+ +
+
var ParsrApi = require('document_parser_api');
+
+var api = new ParsrApi.ProcessingApi()
+var id = id_example; // {{String}} ID of the document
+
+var callback = function(error, data, response) {
+  if (error) {
+    console.error(error);
+  } else {
+    console.log('API called successfully. Returned data: ' + data);
+  }
+};
+api.getQueueStatus(id, callback);
+
+
+ + +
+
using System;
+using System.Diagnostics;
+using IO.Swagger.Api;
+using IO.Swagger.Client;
+using IO.Swagger.Model;
+
+namespace Example
+{
+    public class getQueueStatusExample
+    {
+        public void main()
+        {
+
+            var apiInstance = new ProcessingApi();
+            var id = id_example;  // String | ID of the document
+
+            try
+            {
+                // Get the status of the queue
+                queueStatus result = apiInstance.getQueueStatus(id);
+                Debug.WriteLine(result);
+            }
+            catch (Exception e)
+            {
+                Debug.Print("Exception when calling ProcessingApi.getQueueStatus: " + e.Message );
+            }
+        }
+    }
+}
+
+
+ +
+
<?php
+require_once(__DIR__ . '/vendor/autoload.php');
+
+$api_instance = new Swagger\Client\ApiProcessingApi();
+$id = id_example; // String | ID of the document
+
+try {
+    $result = $api_instance->getQueueStatus($id);
+    print_r($result);
+} catch (Exception $e) {
+    echo 'Exception when calling ProcessingApi->getQueueStatus: ', $e->getMessage(), PHP_EOL;
+}
+?>
+
+ +
+
use Data::Dumper;
+use WWW::SwaggerClient::Configuration;
+use WWW::SwaggerClient::ProcessingApi;
+
+my $api_instance = WWW::SwaggerClient::ProcessingApi->new();
+my $id = id_example; # String | ID of the document
+
+eval { 
+    my $result = $api_instance->getQueueStatus(id => $id);
+    print Dumper($result);
+};
+if ($@) {
+    warn "Exception when calling ProcessingApi->getQueueStatus: $@\n";
+}
+
+ +
+
from __future__ import print_statement
+import time
+import swagger_client
+from swagger_client.rest import ApiException
+from pprint import pprint
+
+# create an instance of the API class
+api_instance = swagger_client.ProcessingApi()
+id = id_example # String | ID of the document
+
+try: 
+    # Get the status of the queue
+    api_response = api_instance.get_queue_status(id)
+    pprint(api_response)
+except ApiException as e:
+    print("Exception when calling ProcessingApi->getQueueStatus: %s\n" % e)
+
+
+ +

Parameters

+ +
Path parameters
+ + + + + + + + +
NameDescription
id* + + +
+
+
+ + String + + +
+ ID of the document +
+
+
+ Required +
+
+
+
+ + + + + +

Responses

+

Status: 200 - Ok. Returns the status of the queue

+ + + +
+
+
+ +
+ +
+
+ +

Status: 201 - Created. Returns the id of the document and links to generated resources.

+ + + +
+
+
+ +
+ +
+
+ + + + + + + + + + + + + +
NameTypeFormatDescription
LocationString
+
+
+ +

Status: 404 - Not Found

+ + + +
+
+ +

Status: 500 - Internal Server Error

+ + + +
+
+ +
+
+
+
+
+ +
+
+
+ + + + + + + + + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/configuration-file.md b/docs/configuration-file.md new file mode 100644 index 00000000..1415e2fd --- /dev/null +++ b/docs/configuration-file.md @@ -0,0 +1,170 @@ +# Configuration File + +- [Configuration File](#Configuration-File) + - [1. Structure](#1-Structure) + - [2. Extractor Config](#2-Extractor-Config) + - [2.1. Extractor Tools](#21-Extractor-Tools) + - [2.2. Language](#22-Language) + - [3. Cleaner Config](#3-Cleaner-Config) + - [4. Output Config](#4-Output-Config) + - [4.1. Output Format](#41-Output-Format) + - [4.2. Granularity](#42-Granularity) + - [4.3. Include Marginals](#43-Include-Marginals) + - [5. Exempli gratia](#5-Exempli-gratia) + +To configure the pipeline and choose what modules will be called and with what parameters, you have to provide a JSON file. +There is only a few required keys: + +- `version` `[Number]` is the version number of the API. +- `extractor` `[Object]` is a bunch of parameters about the extraction. +- `cleaner` `[Array]` is a list of every cleaning tools that will be called. +- `output` `[Object]` contains the list of fromats to export and some other details. + +Cleaning tools have default parameters that work pretty well, but you can override the parameters by providing the in the config. + +The cleaner array may appear unconventionnal but is really easy to use. Every item can be of type: + +- `String`: it's the name of the cleaning tool you want to call. +- `Object`: it's the parameters for the cleaning tool below. + +## 1. Structure + +```js +{ + "version": 0.5, // Version number of the configuration file format + "extractor": { // Extraction options (See section 2.) + "pdf": "extractor-tool", // Select the tool to extract PDF files + "img": "extractor-tool", // Select the tool to extract image files (JPG, PNG, TIFF, etc.) + "language": "lang" // Select the defaut language of your document. This is used to increase the accuracy of OCR tools (See section 2.2) + }, + // The cleaner pipeline consists of a list of modules that will run on given file (See section 3.) + "cleaner": [ + // The first module to run on the document and send the result to the next module + "module-name-1", + // The second module to run. This syntax is also accepted. It will use only the default module options + [ + "module-name-2" + ], + // The thrid module to run with some special options + [ + "module-name-3", + { "option-1": 100, "option-2": true } + ], + ], + // Output options (See section 4.) + "output": { + "formats": { // list of format that will be outputed (See section 4.1.) + "json": true, + "text": false, + "csv": true, + "markdown": false + }, + "granularity": "word | character", // Set the granularity of the output (See section 4.2.) + "includeMarginals": false // Chose whether the output will include headers and footers (See section 4.3.) + } +} +``` + +_This means the module called `fontMerge` will be called, then `removeOutOfPage`, then `removeWhitespace` with some special parameters, etc._ + +## 2. Extractor Config + +### 2.1. Extractor Tools + +Different extractors are available for each input file format. + +- PDF files: three extractors are currently available for PDF files: `pdf2json` which is Open Source and `abbyy` that rely on ABBYY Finereader that is paid software. It is also possible to use `tesseract` in this case. The document will then be converted as an image, so expect the accuracy to be lower on texts. +- Images: two extractors are supporter for images: `tesseract` which is an Open Source OCR and `abbyy` that rely on ABBYY Finereader that is paid software. + +### 2.2. Language + +The language parameter is an option that will be pass to Tesseract when using it. It must be in the [Tesseract language format](https://github.com/tesseract-ocr/tesseract/blob/master/doc/tesseract.1.asc#languages), which is an equivalent of [ISO 639-2/T](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). + +## 3. Cleaner Config + +The pipeline is defined by every module names and their options. Modules will then be run one by one in the same order as in the configuration. + +Module can be called in the full form: + +```json +[ + "module", + { + "option-1": 100, + "option-2": true + } +], +``` + +Or just with there default options: + +```json +[ + "module" +], +``` + +Or just with there default options in the condensed format: + +```json +"module", +``` + +_Note: some modules have dependencies that need to be called before in the pipeline._ + +## 4. Output Config + +### 4.1. Output Format + +The platform can export the following formats: + +- `json` +- `markdown` +- `text` +- `csv` +- ~~`pdf`~~ (planned) + +### 4.2. Granularity + +The `granularity` parameter can be either `word` or `character` and defines at what level of granularity the export will be. + +_Warning: exporting with a character granularity will result on very big Json files (probably more than 10Mo)._ + +### 4.3. Include Marginals + +The `includeMarginals: boolean` parameter allows to chose whether the output will include headers and footers. + +## 5. Exempli gratia + +```json +{ + "version": 0.5, + "extractor": { + "pdf": "pdf2json", + "img": "tesseract", + "language": "eng" + }, + "cleaner": [ + "out-of-page-removal", + "whitespace-removal", + "redundancy-detection", + "reading-order-detection", + "link-detection", + [ "words-to-line", { "maximumSpaceBetweenWords": 100 } ], + "lines-to-paragraph", + "heading-detection", + [ "header-footer-detection", { "maxMarginPercentage": 15 } ], + "hierarchy-detection" + ], + "output": { + "granularity": "word", + "includeMarginals": false, + "formats": { + "json": true, + "text": true, + "csv": true, + "markdown": true + } + } +} +``` diff --git a/docs/create-your-module.md b/docs/create-your-module.md new file mode 100644 index 00000000..6c48d862 --- /dev/null +++ b/docs/create-your-module.md @@ -0,0 +1,28 @@ +# Create your Module + +Creating a custom module can be very useful to add some treament on the document. + +You have two way to do it: + +1) Use the [Remote Module](modules/remote-module.md) that will send the JSON by HTTP and expect the modified JSON as an answer +2) Create a Typescript Module and add it to the pipeline + +## 1. Create a New Typescript Module + +Create a new file in `/server/src/modules/` and name it accordingly. + +You can copy the [template module file](../server/src/modules/TemplateModule.ts) to help you having a boilerplate. It also contains some handy comments. + +## 2. Add to Register + +To add your newly created module to the register, simply open the [Cleaner file](../server/src/Cleaner.ts) `/server/src/Cleaner.ts` and add your module class to the `Cleaner.cleaningToolRegister` attribute. + +## 3. Add it to the Configuration + +If you want your module to run you need to enable it in your [configuration](configuration-file.md#3-Cleaner-Config). + +Simply add a line in the `cleaner` array with the name of your module, and potential options. + +## 4. Run it! + +That's it! Your new awesome module should run and modify the document according to your needs! diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 00000000..4e1a5bd8 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,23 @@ +# Docker + +## Prepare the Docker service + +In the docker installation's configuration on the host machine, increase the amount of active memory allotted for the instance to 4GB. + +## Build the containers + +As for now, 2 containers will be built using docker-compose: + +- Duckling +- Parsr + +To build, them: + +- Clone the repository using `git clone`. +- In the root of the repository, execute `docker-compose build`. + +## Run Parsr + +- In the root of the repository, execute `docker-compose up` + +Please note a docker volume will be created at first launch so that data will be kept at containers restart diff --git a/docs/json-output.md b/docs/json-output.md new file mode 100644 index 00000000..fed3113e --- /dev/null +++ b/docs/json-output.md @@ -0,0 +1,373 @@ +# JSON Output + +This page describes the components of the JSON output file in detail. + +- [JSON Output](#json-output) + - [0. Introduction](#0-introduction) + - [1. `pages` component](#1-pages-component) + - [1.1. Bounding Boxes](#11-bounding-boxes) + - [1.2. Element types](#12-element-types) + - [1.2.1. Text type](#121-text-type) + - [1.2.2. Table type](#122-table-type) + - [1.2.3. List type](#123-list-type) + - [1.2.4. Barcode type](#124-barcode-type) + - [1.2.5. Drawing type](#125-drawing-type) + - [1.3. Properties of an Element](#13-properties-of-an-element) + - [2. `fonts` component](#2-fonts-component) + - [3. `metadata` component](#3-metadata-component) + - [3.1. Key-Value pair metadata](#31-key-value-pair-metadata) + - [3.2. Regex metadata](#32-regex-metadata) + +## 0. Introduction +The output JSON file is composed of the following overall structure: + +```js +{ + "metadata": [/* ... */], + "fonts": [/* ... */], + "pages": [/* ... */], +} +``` + +This file represents the generated output from each of the extraction, cleaning and enrichment modules. + +## 1. `pages` component +`pages` contains an array representing a single page of the document. It contains the following structure: + +```js +"pages" : [ + 0: { + "box": {/* ... */}, // Bounding box of the page, see Section 1.1 + "pageNumber": "1", // The page's page number + "elements": [ // The elements (contents) on the page. + 0: { // The first element. + "box": {/* ... */}, // Bounding box for this particular element. + "type": "heading", // The type of the element; see Section 1.2 + "properties": [/* ... */], // Properties of the element; see Section 1.3 + "metadata": [/* ... */], // Indices of all the metadatas associated with this element. + "content": [/* ... */], // List of contents inside the high level element. + }, + 1: {/* ... */}, + 2: {/* ... */} + ], + }, + 1: { + "box": {/* ... */}, + "pageNumber": "2", + "elements": [ + 0: { + "box": {/* ... */}, + "type": "table", + "properties": [/* ... */], + "metadata": [/* ... */], + "content": [/* ... */], + }, + 1: {/* ... */}, + ], + }, +] +``` + +Each page element contains data relative to the contents of the page in the `elements` section (described in further detail in the Section 1.2), as well as general information pertaining to the properties of that page. +Each element also features a bounding box, which describes its position and size on a given page. + +### 1.1. Bounding Boxes +A bounding box is represented by the following object in the output json: + +```js +{ + "t": 10, // top + "l": 10, // left + "w": 640, // width + "h": 480, // height +} +``` + +### 1.2. Element types +An element is a dinstinguishable block of content inside a page of a document. +All elements contain a Bounding Box object, which is described in the section 1.1. +The list of metadatas attached to an element are listed under the key 'metadata'. +We categorise each content element as one of the following: + +#### 1.2.1. Text type +The text element type is of multiple levels of subtypes: + +- **Paragraph**: The paragraph type contains a list of Lines as elements under the key 'content'. + ```js + { + "id": 1024, + "box": {/* ... */}, + "type": "paragraph", + "content": [/* ... */], // strictly Line type elements + "properties": [/* ... */], + "metadata": [/* ... */], + } + ``` +- **Line**: The line type contains a list of words as elements under the key 'content'. + ```js + { + "id": 1025, + "box": {/* ... */}, + "type": "line", + "content": [/* ... */], // strictly Word type elements + "properties": [/* ... */], + "metadata": [/* ... */], + } + ``` +- **Word**: The word type contains either a list of character type objects or a string under the key 'content'. + ```js + { + "id": 1028, + "box": {/* ... */}, + "type": "word", + "content": [/* ... */], // strictly Character or string type elements + "properties": [/* ... */], + "metadata": [/* ... */], + } + ``` +- **Character**: The character type contains a single character as the content. + ```js + { + "id": 1029, + "box": {/* ... */}, + "type": "character", + "content": "p", // strictly single character string element + "properties": [/* ... */], + "metadata": [/* ... */], + } + ``` +- **Heading**: The heading type is like a paragraph type but with a heading level. + ```js + { + "id": 1024, + "box": {/* ... */}, + "type": "heading", + "content": [/* ... */], // strictly Line type elements + "level": 2 // this is a level 2 heading + "properties": [/* ... */], + "metadata": [/* ... */], + } + ``` + +#### 1.2.2. Table type +The following strucutre defines a table with a single row, single column containing a single cell with a paragraph of text as the cell content. + ```js + { + "id": 4758, + "type": "table", + "properties": , + "order": 15, + "metadata": [/* ... */], + "box": { + "l": 416, + "t": 2411, + "w": 4182, + "h": 3560, + } + "content": [ + "0": { + "id": 2704, + "type": "table-row", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "content": [ + "0": { + "id": 2604, + "type": "table-cell", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "rowspan": 1, + "colspan": 1, + "content": [ + "0": { + "id": 2603, + "type": "paragraph", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "content": [/* ... */], + } + ] + } + ] + } + ] + } + ``` + +#### 1.2.3. List type +The content of a list is a set of paragraphs. +A list of both types bulleted and numbered can be represented by this object. +The distinction between the two can be noted by the boolean value 'isOrdered'. + ```js + { + "id": 169, + "type": "list", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "isOrdered": false, // distinguishes bullet points from numbered lists + "content": [ + "0": { + "id": 168, + "type": "paragraph", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "content": [/* ... */], + }, + "1": { + "id": 169, + "type": "paragraph", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "content": [/* ... */], + } + ] + } + ``` + +#### 1.2.4. Barcode type +A barcode type element represents a barcode element, including the barcode type, as well as the scanned value. + ```js + { + "id": 166, + "type": "barcode", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "codeType": "CODE128", + "codeValue": "0000703082\n" + } + ``` + +#### 1.2.5. Drawing type +A drawing is an SVG element, which for now, can be of subtype 'svg-line'. +An SVG-line represents a line in 2D space with a thickness. + + ```js + { + "id": 166, + "type": "drawing", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "content": [ + 0: { + "id": 167, + "type": "svg-line", + "properties": {/* ... */}, + "metadata": [/* ... */], + "box": {/* ... */}, + "fromX": "12", + "fromY": "12", + "toX": "1558", + "toY": "1570", + "thickness": "7" + } + ] + } + ``` + +### 1.3. Properties of an Element +The properties of an element describe boolean states related to an element. Following provides an example of all the possible values of an element: + +```js +"properties": { + "order": 7, + "isRedundant": true, + "isHeader": true, + "isFooter": true, + "isPageNumber": true, + "bulletList": true, + "titleScores": { + "size": 1.16, + "weight": 0, + "color": 0, + "name": 0, + "italic": 0, + "underline": 0, + }, +} +``` + +## 2. `fonts` component +`fonts` contains an array representing each font formatting in the document. Each formatting style is represented uniquely; for example: Arial 10pt and Arial 10pt Bold will be represented as two seperate elements. + +```js +"fonts" : [ + 0: { + "id": 1, + "name": "Arial", + "size": 10, + "weight": "medium", + "isItalic": false, + "isUnderline": false, + "color": "000000", + }, + 1: { + "id": 2, + "name": "Arial", + "size": 10, + "weight": "bold", + "isItalic": false, + "isUnderline": false, + "color": "000000", + } +] +``` + +## 3. `metadata` component +`metadata` elements represent an outer layer of supplementary information lying on top of the content layer of the document. +Following are the two types of metadatas: + +### 3.1. Key-Value pair metadata +A key-value pair is a group of data labelled as a key-value pair, more precisely divided into two categories: key and value. +The metadata that define elements as key-value pairs group elements defined over two fields. +The key-value pair metadata is generated according to the key-value configuration passed to the pipeline. + +```js +{ + "id": 4, + "type": "key-value", + "keyName": "Policy Number", + "elements": { + "0": 286, + "1": 289, + }, + "data": { + "keyElements": { + "0": 286, + }, + "valueElements": { + "0": 289, + } + } +} +``` + +### 3.2. Regex metadata +A regex metadata represents a regex match, and provides information on the pattern used for the matching, the name of the match, the value found, and the elements on which it was found. + +```js +{ + "id": 89, + "type": "regex", + "elements": [ + 842 + ], + "data": { + "name": "Age", + "regex": "(\\d+)[ -]*(ans|jarige)", + "fullMatch": "24 ans", + "groups": [ + "24", + "ans" + ] + } +} +``` +`fullMatch` shows the match instance to which the entire query was matched, while `groups` lists instances of individual matches. \ No newline at end of file diff --git a/docs/modules/header-footer-detection-module.md b/docs/modules/header-footer-detection-module.md new file mode 100644 index 00000000..70dc2455 --- /dev/null +++ b/docs/modules/header-footer-detection-module.md @@ -0,0 +1,37 @@ +# Header Footer Detection Module + +## Purpose + +Detect the header and footer areas in a document, and set the appropriate properties of all the elements contained within those areas. + +## What it does + +Sets the `isHeader` or `isFooter` property flags for each one of the elements detected as headers or footers respectively in the detected areas. + +## Dependencies + +[Lines To Paragraph Module](lines-to-paragraph-module.md) + +## How it works + +- It detects empty (or white) horizontal bands across the entire document (all pages), which do not contain an element. +- For the footer, the highest of these such bands, not exceeding the value L from the bottom is retained as the footer dividing line. +- For the header, the lowest of these bands is retained using the same algorithm, except that the distance is calculated form the top of the pages. +- The algorithm also detects page number mentions using regexp based matching inside the elements classified as headers or footers. + +## Parameters + +The following two parameters are available: + +1. `maxMarginPercentage`: The percentage of the page upto which (both from the top and the bottom) the algorithm will search for header or footer classification. +2. `ignorePages`: The list of pages to be ignored in the header/footer search. + This typically includes book titles, table of contents, preface, and other pages which do not typically have the same header/footer layout as the rest of the document. + +## Accuracy + +Given a good `maxMarginPercentage`, very good. +The accuracy is directly proportional to the number of pages (samples) available to the algorithm. + +## Limitations + +- An omittance of non-useful pages (like the title page) in the `ignoredPages` list can produce a false negative. diff --git a/docs/modules/heading-detection-module.md b/docs/modules/heading-detection-module.md new file mode 100644 index 00000000..c7007656 --- /dev/null +++ b/docs/modules/heading-detection-module.md @@ -0,0 +1,29 @@ +# Heading Detection Module + +## Purpose + +Detects the headings in a document, given that it already contains Paragraphs, Lines and Words, along with formatting (font size, weight, etc) information. + +## What it does + +Creates new `heading` element types, representing a derivative of Paragraph types, which represent the title of the document, chapter or a (sub)section. + +## Dependencies + +[Lines To Paragraph Module](lines-to-paragraph-module.md) + +## How it works + +- Firstly, the algorithm computes the most used font, font size and font style among all the detected fonts of a document. +- Then, the algorithm allots a weighted score to all the paragraphs who do not exhibit the most used style. +- Finally, each higher of the candidates from the second step are promoted to headings, which are super-set element types over paragraphs. + +## Accuracy + +The overall accuracy is variable. + +## Limitations + +- If the fonts of a document are well detected and coherent with the ground truth, the quality of the heading detection will be high. +- Currently, only the size difference is used to threshold/detect the headings of a document. +- Certain detection extractors (like pdf2json) produce variable quality results for font detection. diff --git a/docs/modules/hierarchy-detection-module.md b/docs/modules/hierarchy-detection-module.md new file mode 100644 index 00000000..ff08b79f --- /dev/null +++ b/docs/modules/hierarchy-detection-module.md @@ -0,0 +1,15 @@ +# Hierarchy Detection Module + +## Purpose + +## What it does + +## Dependencies + +[Lines To Paragraph Module](lines-to-paragraph-module.md) + +## How it works + +## Accuracy + +## Limitations diff --git a/docs/modules/key-value-detection-module.md b/docs/modules/key-value-detection-module.md new file mode 100644 index 00000000..8025cd23 --- /dev/null +++ b/docs/modules/key-value-detection-module.md @@ -0,0 +1,53 @@ +# Key-Value Detection Module + +## Purpose + +Detect pairs of key-value throughout each page of a document, depending on a certain number of key-value patterns passed onto the pipeline in the configuration. + +## What it does + +Generates new `metadata` instances for each match of a key-value pair which validate a match above a certain threshold. + +## Dependencies + +[Words to Line Module](words-to-line-module.md) + +## How it works + +1. All the Line elements of a page are searched for matches with a set of predefined keys in the module parameters using a string matching algorithm based on the Sørensen–Dice coefficient. + Only the matches with a similarity score higher than a threshold are kept as candidates. +2. The resulting set of matched keys are kept as candidates, and a corresponding value is looked for in the vacinity of the found key (on the right of the seperator character: ':' or ';' by default). +3. Each key with its corresponding value is saved as a KeyValueMetadata type and attached to the concerned elements. + +## Parameters + +The configuration of the module is where the key patterns are described. +Following is an example of the configuration of the key-value search module: + +```json +[ + "key-value-detection", + { + "threshold": 0.8, + "keyValueDividerChars": [":", ";"], + "keyPatterns": { + "Name": ["Name", "Fullname", "User"], + "Date of admission": ["ADMISSION DATE & TIME", "Adm Date/Time", "Reg/Admit Date"] + } + } +] +``` + +Here, the `keyValueDividerchars` describes the sets of characters that are used in the input document to seperate the keys from values in a key-pair description. +`keyPatterns` describe objects describing the key names and patterns on which these key names need to be matched. +`threshold` describes the minimum similarity score for candidate substrings matches to pass. + +## Accuracy + +The accuracy is high. +There is a direct corelation with the quality of the readability of the input document, the OCR's output, as well as the threshold for acceptance of candidates. + +## Limitations + +The current implementation supposes that key value pairs are always arranged in a single line and in the form `key : value`. +This makes it applicable to only a few limited cases. diff --git a/docs/modules/lines-to-paragraph-module.md b/docs/modules/lines-to-paragraph-module.md new file mode 100644 index 00000000..ef844ee7 --- /dev/null +++ b/docs/modules/lines-to-paragraph-module.md @@ -0,0 +1,27 @@ +# Lines to Paragraph Module + +## Purpose + +Create paragraphs from a bunch of lines. + +## What it does + +It creates new paragraph elements that contains arrays of line elements. + +## Dependencies + +[Words to Lines](words-to-line-module.md) +[Reading Order Module](reading-order-module.md) + +## How it works + +It simply takes every line one by one according to the reading order and stops and loops if the next line is on another paragraph. + +## Accuracy + +Almost perfect + +## Limitations + +- It depends on the reading order detection quality +- To detect the space between paragraphs, it's currently using an heuristics and doesn't detect automatically according the the interline. So if a paragraph have a large interline spacing, the algo may fail and create one paragraph per line. That said, this rarely occures according to our experience. diff --git a/docs/modules/link-detection-module.md b/docs/modules/link-detection-module.md new file mode 100644 index 00000000..0476588b --- /dev/null +++ b/docs/modules/link-detection-module.md @@ -0,0 +1,27 @@ +# Link Detection Module + +## Purpose + +The Link detection module detects URLs, GoTo links, as well as actionLaunch, actionNamed, actionMovie based links in a PDF document, when used on a pdf2json output. + +## What it does + +It appends a the link found on a word (extracted by pdf2json) to the word's content itself, by transforming the content to an html `` link. + +## Dependencies + +None + +## How it works + +1. It compares each word to three Regex patterns, one looking for URI based link contents, one looking for GoTo links as well as any other Action links. +2. For each case, a suitable representation, like `` for URI's, is attached with the content of the word itself. + +## Accuracy + +All correctly detected links from the extractor are well preserved, and the accuracy can thus be reported to be _pretty good_. + +## Limitations + +- As long as the pdf2json extractor is used, links are preserved, as it is the only currently present extractor with link detection and preservation capacity. +- Links are included in the body of the concerned word, instead of an actual Link element. This is a TODO. diff --git a/docs/modules/list-detection-module.md b/docs/modules/list-detection-module.md new file mode 100644 index 00000000..b7689dcb --- /dev/null +++ b/docs/modules/list-detection-module.md @@ -0,0 +1,27 @@ +# List Detection Module + +**Note**: This module is a work in progress. + +## Purpose + +Detects unordered and ordered lists in the text from paragraphs, using bullet points and numberings as indications. + +## What it does + +Creates new List typed elements in the document based on positively detected paragraphs. + +## Dependencies + +None + +## How it works + +WIP + +## Accuracy + +WIP + +## Limitations + +WIP diff --git a/docs/modules/number-correction-module.md b/docs/modules/number-correction-module.md new file mode 100644 index 00000000..f8e8f9f3 --- /dev/null +++ b/docs/modules/number-correction-module.md @@ -0,0 +1,33 @@ +# Number Correction Module + +## Purpose + +Perform error detection and correction on financial numbers. + +## What it does + +It iterates on every lines and try to apply a correction on numbers such that: + +- `13S` becomes `135` +- `o.oo` becomes `0.00` +- `1,802,86` becomes `1,802.86` +- `443 65` becomes `443.65` + +This module has been made to perform error correction on scanned invoices. + +## Dependencies + +None + +## How it works + +It generates edits and use a scoring system to select the best candidate. +It tries to match a regex that can be change via the module's options and can also take a whitelist of elements that should be accepted as is. + +## Accuracy + +Good + +## Limitations + +- Can have some false positive and correct things that are not errors on numbers. diff --git a/docs/modules/out-of-page-removal-module.md b/docs/modules/out-of-page-removal-module.md new file mode 100644 index 00000000..35f38eb7 --- /dev/null +++ b/docs/modules/out-of-page-removal-module.md @@ -0,0 +1,26 @@ +# Out of Page Removal Module + +## Purpose + +Removes all the elements of the page whose bounding boxes are not physically inside the page. + +## What it does + +It filters out only the elements which are inside the bounding boxes described in the page's `box` property. + +## Dependencies + +None + +## How it works + +It is a simple filter that checks if all the contents of `page.elements` for each page are inside the limits of the page described by `page.box`. + +## Accuracy + +Very high. +The boundaries of each page and element however, need to be correctly specified by the extractor. + +## Limitations + +None diff --git a/docs/modules/reading-order-detection-module.md b/docs/modules/reading-order-detection-module.md new file mode 100644 index 00000000..ce8151ef --- /dev/null +++ b/docs/modules/reading-order-detection-module.md @@ -0,0 +1,29 @@ +# Reading Order Module + +## Purpose + +Detect the reading order of the document. + +## What it does + +It adds the `order` property on every element of the page. Elements can then be sorted according to this property. + +## Dependencies + +None + +## How it works + +It's based on a XY-cut approach with some optimization. + +First, the algorithm will try to find possible vertical cuts in the page between elements. Then, it will perform cuts and try to find possible horizontal cuts in the left part, then the right part. For horizontal cuts, the algorithm will re-assemble blocks if there's some common vertical cuts. This improvement has been made to avoid splitting two columns of text of every line, by choo. + +## Accuracy + +Good + +## Limitations + +- It sometimes fails if bounding boxes are too far from each others. +- It doesnt' work for right to left languages, but can be readapted easily in the code by inverting some functions and sorts. +- It doesn't work when there's a column are in an L shape. diff --git a/docs/modules/redundancy-detection-module.md b/docs/modules/redundancy-detection-module.md new file mode 100644 index 00000000..c19a3346 --- /dev/null +++ b/docs/modules/redundancy-detection-module.md @@ -0,0 +1,29 @@ +# Redundancy Detection Module + +## Purpose + +Detects and remove duplicate textual elements on each page of the document. + +## What it does + +Removes all the elements which exist as duplicates in the document. +If an element **A** has the same content and bounding box as another element **B**, then one of them will be removed. + +## Dependencies + +None + +## How it works + +1. Creates groups of text based on location, by checking between each pairs of elements if they are aligned and overlap vertically. +2. For each group, check if the elements are really duplicates by checking the concurrency of their content and bounding boxes. +3. If yes, we keep only one of the elements in the group and remove the others from the collection. + +## Accuracy + +Good + +## Limitations + +If the contents and location of the two duplicates are not identical, the removal might not take place. +The accuracy in that case will depend on the delta. diff --git a/docs/modules/remote-module.md b/docs/modules/remote-module.md new file mode 100644 index 00000000..d7e9454c --- /dev/null +++ b/docs/modules/remote-module.md @@ -0,0 +1,18 @@ +# Remote Module + +## Purpose + +This module is a bit different than others, because it doesn't change the document by itself. + +It exports the document as [JSON](../json-output.md), call an API with it and expect a modified JSON back. + +## How to use it + +First of all, you need to have a small web server that will handle the API call. +You can use our [Python example](../../demo/python-module/README.md) as a start. + +Your server needs to handle a HTTP `POST` request on the given URL, respond with the modified JSON. + +## Dependencies + +None diff --git a/docs/modules/whitespace-removal-module.md b/docs/modules/whitespace-removal-module.md new file mode 100644 index 00000000..a040d287 --- /dev/null +++ b/docs/modules/whitespace-removal-module.md @@ -0,0 +1,25 @@ +# Whitespace Removal Module + +## Purpose + +Removes textual elements containing only whitespace as content. + +## What it does + +Removes (filters out) all the textual elements not containing any content at all. + +## Dependancies + +None + +## Parameters + +`minWidth`: The minimum width to see if an element is atleast a certain size for it to be taken into consideration as a candidate for removal + +## How it works + +All whitespace textual elements are checked to see if their width is less than `minWidth`, then checked if they are overlapping with other text elements (a very common case), and then deleted. + +## Accuracy + +Good. The module treats the most common cases, but its completeness is based on observation. New edge-cases might appear and will be interesting to treat in the future. diff --git a/docs/modules/words-to-line-module.md b/docs/modules/words-to-line-module.md new file mode 100644 index 00000000..f1b2124d --- /dev/null +++ b/docs/modules/words-to-line-module.md @@ -0,0 +1,26 @@ +# Words to Line Module + +## Purpose + +Create lines from a bunch of words, according to the reading order. + +## What it does + +It creates new line elements that contains arrays of word elements. + +## Dependencies + +[Reading Order Module](reading-order-module.md) + +## How it works + +It simply takes every word one by one according to the reading order and stops and loops if the next word is on another line. + +## Accuracy + +Almost perfect + +## Limitations + +- It depends on the reading order detection quality +- It sometimes fails if lines are close to each others and there's an exponant diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..a3a9bee7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4471 @@ +{ + "name": "extractor", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/runtime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz", + "integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.12.0" + } + }, + "@samverschueren/stream-to-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", + "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "dev": true, + "requires": { + "any-observable": "^0.3.0" + } + }, + "@types/axios": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", + "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", + "dev": true, + "requires": { + "axios": "*" + } + }, + "@types/chai": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.7.tgz", + "integrity": "sha512-2Y8uPt0/jwjhQ6EiluT0XCri1Dbplr0ZxfFXUz+ye13gaqE8u5gL5ppao1JrUYr9cIip5S6MvQzBS7Kke7U9VA==", + "dev": true + }, + "@types/clone": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/clone/-/clone-0.1.30.tgz", + "integrity": "sha1-5zZWSMG0ITalnH1QQGN7O1yDthQ=", + "dev": true + }, + "@types/commander": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/@types/commander/-/commander-2.12.2.tgz", + "integrity": "sha512-0QEFiR8ljcHp9bAbWxecjVRuAMr16ivPiGOw6KFQBVrVd0RQIcM3xKdRisH2EDWgVWujiYtHwhSkSUoAAGzH7Q==", + "dev": true, + "requires": { + "commander": "*" + } + }, + "@types/concaveman": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/concaveman/-/concaveman-1.1.3.tgz", + "integrity": "sha512-G6crIs1efR4OV/Nshgh2w7H0GSsUomloz9Hq0iFysLXsIRX5fHbYGLncIo/RyCljgcpBOqsQdS5e+qJ+ZBVNSg==", + "dev": true + }, + "@types/file-type": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/@types/file-type/-/file-type-10.9.1.tgz", + "integrity": "sha512-oq0fy8Jqj19HofanFsZ56o5anMDUQtFO9B3wfLqM9o42RyCe1WT+wRbSvRbL2l8ARZXNaJturHk0b442+0yi+g==", + "dev": true, + "requires": { + "file-type": "*" + } + }, + "@types/html-entities": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/html-entities/-/html-entities-1.2.16.tgz", + "integrity": "sha512-CI6fHfFvkTtX2Nlr4JBA6yIFTfA4p9E6w9ky64X6PrfXiTALhUh/SOa+Sxvv2p87m+y5AH71lAUrx0lSYx4hKQ==", + "dev": true + }, + "@types/mocha": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", + "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", + "dev": true + }, + "@types/node": { + "version": "10.14.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz", + "integrity": "sha512-DT25xX/YgyPKiHFOpNuANIQIVvYEwCWXgK2jYYwqgaMrYE6+tq+DtmMwlD3drl6DJbUwtlIDnn0d7tIn/EbXBg==", + "dev": true + }, + "@types/pino": { + "version": "5.8.6", + "resolved": "https://registry.npmjs.org/@types/pino/-/pino-5.8.6.tgz", + "integrity": "sha512-3hKjgaAXi8FALboVw1LC+3wiQN+bLZ/byzVgRI65XZxdVLSgjIl3kMVh2etKEdQ96qUgMd6bhNtzrUwh1e1x9g==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/sonic-boom": "*" + } + }, + "@types/sonic-boom": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@types/sonic-boom/-/sonic-boom-0.6.2.tgz", + "integrity": "sha512-vP9Sn1tuz/BTh8L1o776Cbzr+WH4dZGmRXOjQ5L+IVQx40hUmvOS2wfIkqUsID1vL62tThWdlXWIqijwewu3mw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/string-similarity": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-3.0.0.tgz", + "integrity": "sha512-vhHkPKxl0cudrbxr5Dog1HVgUGXtmyYP95qy1da/h5gFEzIqDMN/+SjJAS7/6DEAdeI+AJQX8zrdWXL3wP4FRA==", + "dev": true + }, + "@types/xml2js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.4.tgz", + "integrity": "sha512-O6Xgai01b9PB3IGA0lRIp1Ex3JBcxGDhdO0n3NIIpCyDOAjxcIGQFmkvgJpP8anTrthxOUQjBfLdRRi0Zn/TXA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "an-array-of-english-words": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/an-array-of-english-words/-/an-array-of-english-words-1.3.1.tgz", + "integrity": "sha1-LdBEx5ONdO5pW5mS36rvaHE5n5c=" + }, + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true + }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + } + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + }, + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "clean-deep": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/clean-deep/-/clean-deep-3.0.2.tgz", + "integrity": "sha512-sLUtFxYtHc3jM9pmwyYXOLln0nnQ1OhFrefQ7nqUlva1crHDbi4gVO+nnMSm5jztFXIRDMt+kMNh0mOscbARow==", + "requires": { + "lodash.isempty": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.transform": "^4.6.0" + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + } + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wrap-ansi": "^2.0.0" + } + }, + "clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "combined-stream": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", + "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concaveman": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-1.1.1.tgz", + "integrity": "sha1-bCSCWAslI874L8K+wAoEFebmgWI=", + "requires": { + "monotone-convex-hull-2d": "^1.0.1", + "point-in-polygon": "^1.0.1", + "rbush": "^2.0.1", + "robust-orientation": "^1.1.3", + "tinyqueue": "^1.1.0" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.0.tgz", + "integrity": "sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g==", + "dev": true, + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.0", + "parse-json": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + } + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "csv-stringify": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-5.3.0.tgz", + "integrity": "sha512-VMYPbE8zWz475smwqb9VbX9cj0y4J0PBl59UdcqzLkzXHZZ8dh4Rmbb0ZywsWEtUml4A96Hn7Q5MW9ppVghYzg==", + "requires": { + "lodash.get": "~4.4.2" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-fns": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", + "dev": true + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-rename-keys": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", + "integrity": "sha1-7eeFN9emaivmFRfir5Vtf1ij8dg=", + "requires": { + "kind-of": "^3.0.2", + "rename-keys": "^1.1.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "del": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", + "dev": true, + "requires": { + "globby": "^6.1.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "p-map": "^1.1.1", + "pify": "^3.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" + }, + "eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha1-teEHm1n7XhuidxwKmTvgYKWMmbo=" + }, + "execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + } + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fast-redact": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-1.5.0.tgz", + "integrity": "sha512-Afo61CgUjkzdvOKDHn08qnZ0kwck38AOGcMlvSGzvJbIab6soAP5rdoQayecGCDsD69AiF9vJBXyq31eoEO2tQ==" + }, + "fast-safe-stringify": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", + "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "file-type": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-9.0.0.tgz", + "integrity": "sha512-Qe/5NJrgIOlwijpq3B7BEpzPFcgzggOTagZmkXQY4LA6bsXKTUstK7Wp12lEJ/mLKTpvIZxmIuRcLYWT6ov9lw==" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "flatstr": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.9.tgz", + "integrity": "sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw==" + }, + "fn-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=", + "dev": true + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "^0.2.2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "g-status": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/g-status/-/g-status-2.0.2.tgz", + "integrity": "sha512-kQoE9qH+T1AHKgSSD0Hkv98bobE90ILQcXAF4wvGgsr7uFqNvwmh8j+Lq3l0RVt3E3HjSbv2B9biEGcEtpHLCA==", + "dev": true, + "requires": { + "arrify": "^1.0.1", + "matcher": "^1.0.0", + "simple-git": "^1.85.0" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-own-enumerable-property-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.0.tgz", + "integrity": "sha512-CIJYJC4GGF06TakLg8z4GQKvDsx9EMspVxOYih7LerEL/WosUnFIww45CGfxfeKHqlg3twgUrYRT1O3WQqjGCg==", + "dev": true + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "dev": true, + "requires": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "globule": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", + "integrity": "sha512-g7QtgWF4uYSL5/dn71WxubOrS7JVGCnFPEnoeChJmBnyR9Mw8nGoEwOgJL/RC2Te0WhbsEUCejfH8SZNJ+adYQ==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, + "graceful-fs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", + "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "dev": true + }, + "growl": { + "version": "1.10.5", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", + "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, + "html-entities": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz", + "integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "husky": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", + "integrity": "sha512-86U6sVVVf4b5NYSZ0yvv88dRgBSSXXmHaiq5pP4KDj5JVzdwKgBjEtUPOm8hcoytezFwbU+7gotXNhpHdystlg==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.7", + "execa": "^1.0.0", + "find-up": "^3.0.0", + "get-stdin": "^6.0.0", + "is-ci": "^2.0.0", + "pkg-dir": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^4.0.1", + "run-node": "^1.0.0", + "slash": "^2.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + } + } + } + }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "in-publish": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz", + "integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E=", + "dev": true + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=", + "dev": true + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dev": true, + "requires": { + "symbol-observable": "^1.1.0" + } + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "dev": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "dev": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, + "js-base64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", + "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "dev": true, + "requires": { + "invert-kv": "^1.0.0" + } + }, + "leche": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/leche/-/leche-2.2.3.tgz", + "integrity": "sha512-VXI3BEfDlweuciVHG/HYQ/39iaShaiQiMm8TdEHz3gUXCjkfYXYr1WtQnIaKa4Ig+80qLF4iteBCD8a5CTZSVw==", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "lint-staged": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-8.1.5.tgz", + "integrity": "sha512-e5ZavfnSLcBJE1BTzRTqw6ly8OkqVyO3GL2M6teSmTBYQ/2BuueD5GIt2RPsP31u/vjKdexUyDCxSyK75q4BDA==", + "dev": true, + "requires": { + "chalk": "^2.3.1", + "commander": "^2.14.1", + "cosmiconfig": "^5.0.2", + "debug": "^3.1.0", + "dedent": "^0.7.0", + "del": "^3.0.0", + "execa": "^1.0.0", + "find-parent-dir": "^0.3.0", + "g-status": "^2.0.2", + "is-glob": "^4.0.0", + "is-windows": "^1.0.2", + "listr": "^0.14.2", + "listr-update-renderer": "^0.5.0", + "lodash": "^4.17.11", + "log-symbols": "^2.2.0", + "micromatch": "^3.1.8", + "npm-which": "^3.0.1", + "p-map": "^1.1.1", + "path-is-inside": "^1.0.2", + "pify": "^3.0.0", + "please-upgrade-node": "^3.0.2", + "staged-git-files": "1.1.2", + "string-argv": "^0.0.2", + "stringify-object": "^3.2.2", + "yup": "^0.26.10" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "listr": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", + "dev": true, + "requires": { + "@samverschueren/stream-to-observable": "^0.3.0", + "is-observable": "^1.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.5.0", + "listr-verbose-renderer": "^0.5.0", + "p-map": "^2.0.0", + "rxjs": "^6.3.3" + }, + "dependencies": { + "p-map": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", + "dev": true + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^2.3.0", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + } + } + } + }, + "listr-verbose-renderer": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", + "dev": true, + "requires": { + "chalk": "^2.4.1", + "cli-cursor": "^2.1.0", + "date-fns": "^1.27.2", + "figures": "^2.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "dependencies": { + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + } + } + }, + "lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "dev": true + }, + "lodash.every": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.every/-/lodash.every-4.6.0.tgz", + "integrity": "sha1-64mYS+vENkJ5uzrvu9HKGb+mxqc=" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=" + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=" + }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.map": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", + "integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM=" + }, + "lodash.maxby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.maxby/-/lodash.maxby-4.6.0.tgz", + "integrity": "sha1-CCJABo88eiJ6oAqDgOTzjPB4bj0=" + }, + "lodash.transform": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", + "integrity": "sha1-EjBkIvYzJK7YSD0/ODMrX2cFR6A=" + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "log-update": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha1-iDKP19HOeTiykoN0bwsbwSayRwg=", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "cli-cursor": "^2.0.0", + "wrap-ansi": "^3.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=", + "dev": true, + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0" + } + } + } + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "^1.0.0" + } + }, + "markdown-table": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-1.1.3.tgz", + "integrity": "sha512-1RUZVgQlpJSPWYbFSpmudq5nHY1doEIv89gBtF0s4gW1GF2XorxcA/70M5vq7rLv0a6mhOUccRsqkwhwLCIQ2Q==" + }, + "matcher": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz", + "integrity": "sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.4" + } + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime-db": { + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==" + }, + "mime-types": { + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", + "requires": { + "mime-db": "~1.38.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", + "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", + "dev": true, + "requires": { + "browser-stdout": "1.3.1", + "commander": "2.15.1", + "debug": "3.1.0", + "diff": "3.5.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.5", + "he": "1.1.1", + "minimatch": "3.0.4", + "mkdirp": "0.5.1", + "supports-color": "5.4.0" + }, + "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", + "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "monotone-convex-hull-2d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/monotone-convex-hull-2d/-/monotone-convex-hull-2d-1.0.1.tgz", + "integrity": "sha1-R/Xa6t88Sv03dkuqGqh4ekDu4Iw=", + "requires": { + "robust-orientation": "^1.1.3" + } + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "dev": true + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, + "node-sass": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.12.0.tgz", + "integrity": "sha512-A1Iv4oN+Iel6EPv77/HddXErL2a+gZ4uBeZUy+a8O35CFYTXhgA8MgLCWBtwpGZdCvTvQ9d+bQxX/QC36GDPpQ==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.11", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "^2.2.4", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-path": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", + "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", + "dev": true, + "requires": { + "which": "^1.2.10" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "^2.0.0" + } + }, + "npm-which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", + "integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=", + "dev": true, + "requires": { + "commander": "^2.9.0", + "npm-path": "^2.0.2", + "which": "^1.2.10" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "omit-deep": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/omit-deep/-/omit-deep-0.3.0.tgz", + "integrity": "sha1-IcivNJm8rdKWUaIyy8rLxSRF6+w=", + "requires": { + "is-plain-object": "^2.0.1", + "unset-value": "^0.1.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + }, + "unset-value": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-0.1.2.tgz", + "integrity": "sha1-UGgQuGfyfCpabpsEgzYx9t5Y0xA=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + } + } + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, + "os-locale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", + "integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=", + "dev": true, + "requires": { + "lcid": "^1.0.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "pino": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/pino/-/pino-5.12.2.tgz", + "integrity": "sha512-EunVRDkw/eQzgAudJiZuqzEQ20hDezixLRLrdxUMBzavvt5ot3vep7K8swRvXSgj2bKtbOmoHnrRMtYzRjfITQ==", + "requires": { + "fast-redact": "^1.4.4", + "fast-safe-stringify": "^2.0.6", + "flatstr": "^1.0.9", + "pino-std-serializers": "^2.3.0", + "quick-format-unescaped": "^3.0.2", + "sonic-boom": "^0.7.3" + } + }, + "pino-pretty": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-2.6.0.tgz", + "integrity": "sha512-0mrKDOCI35tb8Wjrg793ZE+AyFZYivUH7ekJdcd41RJik32BT7cgLsq/Kjh8+lvQa+Mx2PghptN1x0tsO2Trvw==", + "requires": { + "args": "^5.0.0", + "chalk": "^2.3.2", + "dateformat": "^3.0.3", + "fast-json-parse": "^1.0.3", + "fast-safe-stringify": "^2.0.6", + "jmespath": "^0.15.0", + "pump": "^3.0.0", + "readable-stream": "^3.0.6", + "split2": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "pino-std-serializers": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-2.4.0.tgz", + "integrity": "sha512-ysT2ylXu1aEec9k8cm/lz7emBcfpdxFWHqvHeGXf1wvfw7TKPMGhLWwS+ciHw6u4ffnmV+pkAMF4MUIZmZZdSg==" + }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + }, + "dependencies": { + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + } + } + }, + "please-upgrade-node": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", + "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, + "point-in-polygon": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.0.1.tgz", + "integrity": "sha1-1Ztk6P7kHElFiqyCtWcYxZV7Kvc=" + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, + "prettier": { + "version": "1.16.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz", + "integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "property-expr": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz", + "integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "psl": { + "version": "1.1.31", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", + "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "quick-format-unescaped": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.2.tgz", + "integrity": "sha512-FXTaCkwvpIlkdKeGDNgcq07SXWS383noQUuZjvdE1QcTt+eLuqof6/BDiEPqB59FWLie/l91+HtlJSw7iCViSA==" + }, + "quickselect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz", + "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==" + }, + "rbush": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz", + "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", + "requires": { + "quickselect": "^1.0.1" + } + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.2.0.tgz", + "integrity": "sha512-RV20kLjdmpZuTF1INEb9IA3L68Nmi+Ri7ppZqo78wj//Pn62fCoJyV9zalccNzDD/OuJpMG4f+pfMl8+L6QdGw==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "regenerator-runtime": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", + "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==", + "dev": true + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "rename-keys": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", + "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==" + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "dev": true + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "requires": { + "path-parse": "^1.0.6" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true + }, + "rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "robust-orientation": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/robust-orientation/-/robust-orientation-1.1.3.tgz", + "integrity": "sha1-2v9bANO+TmByLw6cAVbvln8cIEk=", + "requires": { + "robust-scale": "^1.0.2", + "robust-subtract": "^1.0.0", + "robust-sum": "^1.0.0", + "two-product": "^1.0.2" + } + }, + "robust-scale": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/robust-scale/-/robust-scale-1.0.2.tgz", + "integrity": "sha1-d1Ey7QlULQKOWLLMecBikLz3jDI=", + "requires": { + "two-product": "^1.0.2", + "two-sum": "^1.0.0" + } + }, + "robust-subtract": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-subtract/-/robust-subtract-1.0.0.tgz", + "integrity": "sha1-4LFk4e2LpOOl3aRaEgODSNvtPpo=" + }, + "robust-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/robust-sum/-/robust-sum-1.0.0.tgz", + "integrity": "sha1-FmRuUlKStNJdgnV6KGlV4Lv6U9k=" + }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "dev": true + }, + "rxjs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz", + "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass-graph": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.4.tgz", + "integrity": "sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k=", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^7.0.0" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true + }, + "set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "simple-git": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-1.110.0.tgz", + "integrity": "sha512-UYY0rQkknk0P5eb+KW+03F4TevZ9ou0H+LoGaj7iiVgpnZH4wdj/HTViy/1tNNkmIPcmtxuBqXWiYt2YwlRKOQ==", + "dev": true, + "requires": { + "debug": "^4.0.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "dev": true, + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "sonic-boom": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-0.7.3.tgz", + "integrity": "sha512-A9EyoIeLD+g9vMLYQKjNCatJtAKdBQMW03+L8ZWWX/A6hq+srRCwdqHrBD1R8oSMLXov3oHN13dljtZf12q2Ow==", + "requires": { + "flatstr": "^1.0.9" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "dev": true, + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-support": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.11.tgz", + "integrity": "sha512-//sajEx/fGL3iw6fltKMdPvy8kL3kJ2O3iuYlRoT3k9Kb4BjOoZ+BZzaNHeuaruSt+Kf3Zk9tnfAQg9/AJqUVQ==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz", + "integrity": "sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g==", + "dev": true + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "split2": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.1.1.tgz", + "integrity": "sha512-emNzr1s7ruq4N+1993yht631/JH+jaj0NYBosuKmLcq+JkGQ9MmTw1RB1fGaTCzUuseRIClrlSLHRNYGwWQ58Q==", + "requires": { + "readable-stream": "^3.0.0" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "staged-git-files": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.1.2.tgz", + "integrity": "sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA==", + "dev": true + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "string-argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz", + "integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=", + "dev": true + }, + "string-similarity": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-1.2.2.tgz", + "integrity": "sha512-IoHUjcw3Srl8nsPlW04U3qwWPk3oG2ffLM0tN853d/E/JlIvcmZmDY2Kz5HzKp4lEi2T7QD7Zuvjq/1rDw+XcQ==", + "requires": { + "lodash.every": "^4.6.0", + "lodash.flattendeep": "^4.4.0", + "lodash.foreach": "^4.5.0", + "lodash.map": "^4.6.0", + "lodash.maxby": "^4.6.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + }, + "svgson": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/svgson/-/svgson-3.1.0.tgz", + "integrity": "sha512-7DbA8d02COOjp+BACdxrPJNaIrlKROhNloktJF6lbeVz4syVLAKv8ViwHScwXK0RSLeTFND4OuJFpRlrdoqRuw==", + "requires": { + "clean-deep": "3.0.2", + "deep-rename-keys": "^0.2.1", + "omit-deep": "0.3.0", + "xml-reader": "2.4.3" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + }, + "synchronous-promise": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.7.tgz", + "integrity": "sha512-16GbgwTmFMYFyQMLvtQjvNWh30dsFe1cAW5Fg1wm5+dg84L9Pe36mftsIRU95/W2YsISxsz/xq4VB23sqpgb/A==", + "dev": true + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, + "tinyqueue": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", + "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==" + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "dev": true, + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=", + "dev": true + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, + "ts-node": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz", + "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "buffer-from": "^1.1.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==" + }, + "tslint": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.15.0.tgz", + "integrity": "sha512-6bIEujKR21/3nyeoX2uBnE8s+tMXCQXhqMmaIPJpHmXJoBJPTLcI7/VHRtUwMhnLVdwLqqY3zmd8Dxqa5CVdJA==", + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.13.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "requires": { + "tslib": "^1.8.1" + } + }, + "tsv": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/tsv/-/tsv-0.2.0.tgz", + "integrity": "sha1-koaaPLX1AzLz3JD8qCvmZ9tvctY=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "two-product": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/two-product/-/two-product-1.0.2.tgz", + "integrity": "sha1-Z9ldSyV6kh4stL16+VEfkIhSLqo=" + }, + "two-sum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/two-sum/-/two-sum-1.0.0.tgz", + "integrity": "sha1-MdPzIjnk9zHsqd+RVeKyl/AIq2Q=" + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "typescript": { + "version": "3.3.4000", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz", + "integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==", + "dev": true + }, + "union-value": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", + "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", + "integrity": "sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8=", + "dev": true + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "dev": true, + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xml-lexer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", + "integrity": "sha1-UYGTpKozTVj8fSSLVJB5uJkH4EY=", + "requires": { + "eventemitter3": "^2.0.0" + } + }, + "xml-reader": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", + "integrity": "sha1-n4EMr3xCWlqvuEixxFEDyecddTA=", + "requires": { + "eventemitter3": "^2.0.0", + "xml-lexer": "^0.2.2" + } + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, + "y18n": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", + "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + }, + "yargs": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", + "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "cliui": "^3.2.0", + "decamelize": "^1.1.1", + "get-caller-file": "^1.0.1", + "os-locale": "^1.4.0", + "read-pkg-up": "^1.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^1.0.2", + "which-module": "^1.0.0", + "y18n": "^3.2.1", + "yargs-parser": "^5.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yargs-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", + "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "dev": true, + "requires": { + "camelcase": "^3.0.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", + "dev": true + } + } + }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, + "yup": { + "version": "0.26.10", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.26.10.tgz", + "integrity": "sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw==", + "dev": true, + "requires": { + "@babel/runtime": "7.0.0", + "fn-name": "~2.0.1", + "lodash": "^4.17.10", + "property-expr": "^1.5.0", + "synchronous-promise": "^2.0.5", + "toposort": "^2.0.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..b5ba52cf --- /dev/null +++ b/package.json @@ -0,0 +1,93 @@ +{ + "name": "parsr", + "version": "1.0.0", + "description": "Turn your documents into data!", + "main": "dist/bin/index.js", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/axa-group/Parsr.git" + }, + "author": "AXA rev", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/axa-group/Parsr/issues" + }, + "homepage": "https://github.com/axa-group/Parsr#readme", + "directories": { + "test": "test" + }, + "scripts": { + "test": "mocha -r ts-node/register **/*.spec.ts", + "build:ts": "tsc --outDir ./dist", + "build:ts:watch": "tsc -w --outDir ./dist", + "install:api": "npm install --prefix api/server", + "start:web": "npm run build:ts && cd demo/web-viewer && npm run start", + "run:debug": "ts-node server/bin/index.ts", + "lint": "tslint --project . && tslint --project ./api/server", + "lint:fix": "tslint --fix --project . && tslint --fix --project ./api/server", + "start:api": "npm run build:ts && npm run --prefix api/server start", + "start:front": "cd demo/web-viewer && node index.js", + "start": "npm run start:api & npm run start:front", + "format": "prettier --write --list-different '{,!(dist|demo/web-viewer/public)/**/}*.{js,md,ts,css,scss}'" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged && npm test && npm audit", + "pre-push": "npm test && npm audit" + } + }, + "lint-staged": { + "linters": { + "{,!(dist|demo/web-viewer/public)/**/}*.{js,md,ts,css,scss}": [ + "npm run format", + "git add" + ], + "{,!(dist|demo/web-viewer/public)/**/}*.{ts}": [ + "tslint --fix", + "git add" + ] + } + }, + "dependencies": { + "an-array-of-english-words": "^1.3.1", + "axios": "^0.19.0", + "clone": "^2.1.2", + "commander": "^2.19.0", + "concaveman": "^1.1.1", + "csv-stringify": "^5.3.0", + "file-type": "^9.0.0", + "html-entities": "^1.2.1", + "markdown-table": "^1.1.3", + "pino": "^5.12.0", + "pino-pretty": "^2.6.0", + "request": "^2.88.0", + "string-similarity": "^1.2.1", + "svgson": "^3.1.0", + "tslint": "^5.14.0", + "tsv": "^0.2.0", + "xml2js": "^0.4.19" + }, + "devDependencies": { + "@types/axios": "^0.14.0", + "@types/chai": "^4.1.4", + "@types/clone": "^0.1.30", + "@types/commander": "^2.12.2", + "@types/concaveman": "^1.1.3", + "@types/file-type": "^10.9.1", + "@types/html-entities": "^1.2.16", + "@types/mocha": "^5.2.5", + "@types/node": "^10.14.4", + "@types/pino": "^5.8.6", + "@types/string-similarity": "^3.0.0", + "@types/xml2js": "^0.4.4", + "chai": "^4.1.2", + "husky": "^1.3.1", + "leche": "^2.2.3", + "lint-staged": "^8.1.5", + "mocha": "^5.2.0", + "node-sass": "^4.12.0", + "prettier": "^1.16.4", + "ts-node": "^7.0.1", + "typescript": "^3.0.1" + } +} diff --git a/server/bin/index.ts b/server/bin/index.ts new file mode 100644 index 00000000..867d5f96 --- /dev/null +++ b/server/bin/index.ts @@ -0,0 +1,298 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as child_process from 'child_process'; +import * as commander from 'commander'; +import * as filetype from 'file-type'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Cleaner } from '../src/Cleaner'; +import { + CsvExporter, + JsonExporter, + MarkdownExporter, + PdfExporter, + TextExporter, +} from '../src/exporters'; +import { AbbyyTools } from '../src/extractors/abbyy/AbbyyTools'; +import { AbbyyToolsXml } from '../src/extractors/abbyy/AbbyyToolsXml'; +import { JsonExtractor } from '../src/extractors/json/JsonExtractor'; +import { PdfJsonExtractor } from '../src/extractors/pdf2json/PdfJsonExtractor'; +import { TesseractExtractor } from '../src/extractors/tesseract/TesseractExtractor'; +import { Orchestrator } from '../src/Orchestrator'; +import { Config } from '../src/types/Config'; +import { Document } from '../src/types/DocumentRepresentation/'; +import * as utils from '../src/utils'; +import logger from '../src/utils/Logger'; + +/** + * Runs the CLI handler for Parsr + */ +function main(): void { + commander + .option('-f, --input-file ', 'Input file to be processed') + .option( + '-o, --output-folder ', + 'Location of the folder where the output will be stored', + ) + .option('-n, --document-name [name]', 'Name of the document') + .option( + '-c, --config ', + "The file's path from which the application's parameres will be loaded", + ) + .option( + '-l, --log-level ', + 'Verbosity level: debug, info (default), warn, error', + 'info', + ) + .option('-p, --pretty-logs', 'Make logs look pretty but unreadable for a machine') + .parse(process.argv); + + logger.init(!!commander.prettyLogs); + logger.level = commander.logLevel; + + printVersion(); + + let filePath: string = path.resolve(commander.inputFile); + const outputFolder: string = path.resolve(commander.outputFolder); + const documentName: string = commander.documentName; + const configPath: string = path.resolve(commander.config); + let fileType: { ext: string; mime: string } = filetype(fs.readFileSync(filePath)); + const configStr: string = fs.readFileSync(configPath, 'utf-8'); + const config: Config = new Config(JSON.parse(configStr)); + + logger.info('Using config:'); + logger.info(utils.prettifyObject(config)); + + const cleaner: Cleaner = new Cleaner(config); + let orchestrator: Orchestrator; + + if (!fileType) { + fileType = { ext: '', mime: '' }; + const a = filePath.split('.'); + fileType.ext = a[a.length - 1]; + } + + /** + * Decide file type and associate a suitable orchestrator + */ + if (fileType.ext === 'xml') { + orchestrator = new Orchestrator(new AbbyyToolsXml(config), cleaner); + } else if (fileType.ext === 'pdf') { + orchestrator = getPdfExtractor(); + } else if (fileType.mime.slice(0, 5) === 'image') { + orchestrator = getImgExtractor(); + } else if (fileType.ext === 'json') { + orchestrator = getJsonExtractor(); + } else { + process.exit(1); + throw new Error('Input file is neither a PDF nor an image'); + } + + /** + * Run the extraction pipeline on the file + */ + runOrchestrator(); + + /** + * Run the pipeline - go through the extraction, cleaning, and enrichment modules. + * + * @remarks + * This method contains the primary pipeline call itself. + */ + function runOrchestrator() { + orchestrator + .run(filePath) + .then((doc: Document) => { + const nbTexts = doc.pages.map(p => p.elements.length).reduce((a, b) => a + b); + logger.debug('nbTexts: ' + nbTexts); + + if (nbTexts === 0) { + if (config.extractor.img === 'tesseract') { + orchestrator = pdfWithTesseract(); + } else { + orchestrator = new Orchestrator(new AbbyyTools(config), cleaner); + } + + return orchestrator.run(filePath); + } else { + return doc; + } + }) + .then((doc: Document) => { + const promises: Array> = []; + + if (config.output.formats.json) { + promises.push( + new JsonExporter(doc, config.output.granularity).export( + `${outputFolder}/${documentName}.json`, + ), + ); + } + + // if (config.output.formats['json-compact']) { + // promises.push( + // new JsonCompactExporter(doc).export( + // `${outputFolder}/${documentName}.compact.json`, + // ), + // ); + // } + + if (config.output.formats.text) { + promises.push( + new TextExporter(doc, config.output.includeMarginals).export( + `${outputFolder}/${documentName}.txt`, + ), + ); + } + + if (config.output.formats.markdown) { + promises.push( + new MarkdownExporter(doc, config.output.includeMarginals).export( + `${outputFolder}/${documentName}.md`, + ), + ); + } + + // if (config.output.formats.xml) { + // promises.push( + // new XmlExporter(doc).export( + // `${outputFolder}/${documentName}.md` + // ) + // ); + // } + + // if (config.output.formats.confidences) { + // promises.push( + // new ConfidencesExporter(doc).export( + // `${outputFolder}/${documentName}.confidences` + // ) + // ); + // } + + if (config.output.formats.csv) { + promises.push(new CsvExporter(doc).export(`${outputFolder}/${documentName}.csv`)); + } + + if (config.output.formats.pdf) { + promises.push( + new PdfExporter(doc, config.output.includeMarginals).export( + `${outputFolder}/${documentName}.pdf`, + ), + ); + } + + logger.debug('Done'); + return Promise.all(promises); + }) + .catch(err => { + logger.error(err); + }); + } + + /** + * Returns the pdf extraction orchestrator depending on the extractor selection made in the configuration. + * + * @returns The Orchestrator instance + */ + function getPdfExtractor(): Orchestrator { + if (config.extractor.pdf === 'abbyy') { + return new Orchestrator(new AbbyyTools(config), cleaner); + } else if (config.extractor.pdf === 'tesseract') { + return pdfWithTesseract(); + } else { + return new Orchestrator(new PdfJsonExtractor(config), cleaner); + } + } + + /** + * Returns the json extraction orchestrator depending on the extractor selection made in the configuration. + * + * @returns The Orchestrator instance + */ + function getJsonExtractor(): Orchestrator { + return new Orchestrator(new JsonExtractor(config), cleaner); + } + + /** + * Returns the img extraction orchestrator depending on the extractor selection made in the configuration. + * + * @returns The Orchestrator instance + */ + function getImgExtractor(): Orchestrator { + if (config.extractor.img === 'tesseract') { + return new Orchestrator(new TesseractExtractor(config), cleaner); + } else { + return new Orchestrator(new AbbyyTools(config), cleaner); + } + } + + /** + * Returns the pdf file extraction orchestrator using tesseract as the extractor. + * First, the pdf is sampled for it to be converted into an image, then, an image extraction orchestrator is returned. + * + * @returns The Orchestrator instance + */ + function pdfWithTesseract(): Orchestrator { + const tifFilePath = filePath + '.tiff'; + const ret = child_process.spawnSync(utils.getConvertPath(), [ + '-density', + '200x200', + '-compress', + 'Fax', + filePath, + tifFilePath, + ]); + if (ret.status !== 0) { + logger.error(ret.stderr); + throw new Error( + 'ImageMagick failure: impossible to convert pdf to images (is ImageMagick installed?)', + ); + } + filePath = tifFilePath; + return new Orchestrator(new TesseractExtractor(config), cleaner); + } +} + +/** + * Outputs the current version of the code into the logger. + * Uses the git repository to retrieve this information using the command 'git'. + * If the git command fails, a failure message is logged. + */ +function printVersion() { + try { + const message = child_process + .spawnSync( + 'git', + ['--no-pager', 'show', '-s', '--no-color', '--format=[%h] %d - %s - (%cd, %cn <%ce>)'], + { encoding: 'utf-8' }, + ) + .output.join('') + .trim(); + logger.info('Current version: ' + message); + } catch (e) { + logger.info('No info found about the current version'); + } +} + +/** + * Exits the program printing the exit code. + */ +process.on('exit', code => { + return logger.info(`Exiting with code ${code}`); +}); + +main(); diff --git a/server/configKeyValueSearch.json b/server/configKeyValueSearch.json new file mode 100644 index 00000000..084b23f1 --- /dev/null +++ b/server/configKeyValueSearch.json @@ -0,0 +1,107 @@ +{ + "version": 0.5, + "extractor": { + "pdf": "pdf2json", + "img": "tesseract", + "language": "eng" + }, + "cleaner": [ + "out-of-page-removal", + "whitespace-removal", + "redundancy-detection", + "reading-order-detection", + "link-detection", + ["words-to-line", { "maximumSpaceBetweenWords": 100 }], + "lines-to-paragraph", + "heading-detection", + ["header-footer-detection", { "maxMarginPercentage": 15 }], + "hierarchy-detection", + [ + "key-value-detection", + { + "keyValueDividerChars": [":", ";"], + "keyPatterns": { + "Name": ["Name", "PATIENT NAME", "Patient Name", "PATIENT", "User"], + "Date of admission": [ + "Registered At", + "ADMISSION DATE & TIME", + "Adm Date/Time", + "ADMISSION DATE", + "Reg/Admit Date", + "ADMISSION DATE TIME" + ], + "Date of discharge": [ + "Discharged At", + "DISCHARGE DATE AND TIME", + "DISCHARGE DATE & TIME", + "Dis Date/Time", + "DISCHARGE DATE", + "Discharge Date", + "DISCHARGE DATE TIME" + ], + "D.O.B or Age": ["DOB", "SEX / AGE", "Age / Gender"], + "Passport or National ID": [ + "IC No", + "IC / PASSPORT NO", + "ID No.", + "NRIC / PASSPORT", + "IC/PP/BC No.", + "NRIC/Passport" + ], + "Admission doctor": ["ADMITTING DOCTOR", "Admitting Dr", "DOCTOR", "ATTENDING DOCTOR"], + "Ward type": ["WARD / ROOM / CLASS", "Ward/Rm/Bed/Type", "WARD/BED", "BED NO / WARD"], + "Patient address": ["Patient Add", "ADDRESS"], + "Admission Number": ["Admission No", "ADMISSION NO"], + "Account": ["Account", "PATIENT ACCOUNT NO"], + "BED TYPE": ["BED TYPE"], + "BILL NO": ["BILL NO", "Bill No."], + "Invoice Date": ["INVOICE DATE", "Bill Date/Time", "BILL DATE"], + "Invoice Number": ["INVOICE NO"], + "Bill Type": ["Bill Type"], + "Cashier ID": ["CASHIER ID"], + "Charge Type": ["CHARGE TYPE", "Charge Type"], + "CO. Guarantor": ["CO. GUARANTOR"], + "Credit Term": ["CREDIT TERM", "Credit Term"], + "Consultant": ["Consultant"], + "Date": ["DATE", "Date / Time"], + "Debtor Code": ["DEBTOR CODE"], + "Debtor Name": ["DEBTOR NAME"], + "Employee Name": ["EMPLOYEE NAME"], + "Employee No": ["EMPLOYEE NO"], + "Financial Type": ["FINANCIAL TYPE"], + "GL Number": ["GL No", "GL Ref No", "CLAIMS/PLY/GL NO", "GL REFERENCE NO", "LG Ref No"], + "Lab No": ["Lab No"], + "Length of Stay": ["Length of Stay(LOS)"], + "Location": ["Location"], + "Medical Record Number": ["MEDICAL RECORD NO", "MRN"], + "PRN": ["PRN"], + "Page": ["Page", "PAGE"], + "Phone": ["Phone", "TEL."], + "Policy Number": ["Policy No", "POLICY / REF NO"], + "Prepared By": ["Prepared By"], + "Printed": ["Printed"], + "Registration Number": ["REGISTRATION NO"], + "Relation": ["RELATION"], + "Received": ["Received"], + "Reported": ["Reported"], + "Title": ["Title"], + "Visit ID": ["VISIT ID", "Visit ID"], + "Visite TYPE": ["VISIT TYPE", "Visit Type"], + "Visit No": ["Visit No"] + }, + "threshold": 0.8 + } + ] + ], + "output": { + "granularity": "word", + "includeMarginals": false, + "formats": { + "json": true, + "text": true, + "csv": true, + "markdown": true, + "pdf": false + } + } +} diff --git a/server/defaultConfig.json b/server/defaultConfig.json new file mode 100644 index 00000000..9b27b70e --- /dev/null +++ b/server/defaultConfig.json @@ -0,0 +1,44 @@ +{ + "version": 0.5, + "extractor": { + "pdf": "pdf2json", + "img": "tesseract", + "language": ["eng", "fra"] + }, + "cleaner": [ + "out-of-page-removal", + "whitespace-removal", + "redundancy-detection", + "reading-order-detection", + "link-detection", + ["words-to-line", { "maximumSpaceBetweenWords": 100 }], + "lines-to-paragraph", + "heading-detection", + ["header-footer-detection", { "maxMarginPercentage": 15 }], + "hierarchy-detection", + ["regex-matcher", { + "queries": [ + { + "label": "Car", + "regex": "([A-Z]{2}\\-[\\d]{3}\\-[A-Z]{2})" + }, { + "label": "Age", + "regex": "(\\d+)[ -]*(ans|jarige)" + }, { + "label": "Percent", + "regex": "([\\-]?(\\d)+[\\.\\,]*(\\d)*)[ ]*(%|per|percent|pourcent|procent)" + }] + }] + ], + "output": { + "granularity": "word", + "includeMarginals": false, + "formats": { + "json": true, + "text": true, + "csv": true, + "markdown": true, + "pdf": false + } + } +} diff --git a/server/remoteModuleConfig.json b/server/remoteModuleConfig.json new file mode 100644 index 00000000..2e361c39 --- /dev/null +++ b/server/remoteModuleConfig.json @@ -0,0 +1,46 @@ +{ + "version": 0.5, + "extractor": { + "pdf": "pdf2json", + "img": "tesseract", + "language": ["eng", "fra"] + }, + "cleaner": [ + "out-of-page-removal", + "whitespace-removal", + "redundancy-detection", + "reading-order-detection", + "link-detection", + ["words-to-line", { "maximumSpaceBetweenWords": 100 }], + "lines-to-paragraph", + ["remote", { + "url": "http://localhost:8888" + }], + "heading-detection", + ["header-footer-detection", { "maxMarginPercentage": 15 }], + "hierarchy-detection", + ["regex-matcher", { + "queries": [ + { + "label": "Car", + "regex": "([A-Z]{2}\\-[\\d]{3}\\-[A-Z]{2})" + }, { + "label": "Age", + "regex": "(\\d+)[ -]*(ans|jarige)" + }, { + "label": "Percent", + "regex": "([\\-]?(\\d)+[\\.\\,]*(\\d)*)[ ]*(%|per|percent|pourcent|procent)" + }] + }] + ], + "output": { + "granularity": "word", + "includeMarginals": false, + "formats": { + "json": true, + "text": true, + "csv": true, + "markdown": true + } + } +} diff --git a/server/src/Cleaner.ts b/server/src/Cleaner.ts new file mode 100644 index 00000000..ed56252a --- /dev/null +++ b/server/src/Cleaner.ts @@ -0,0 +1,211 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { HeaderFooterDetectionModule } from './modules/HeaderFooterDetectionModule'; +import { HeadingDetectionModule } from './modules/HeadingDetectionModule'; +import { HierarchyDetectionModule } from './modules/HierarchyDetectionModule'; +import { KeyValueDetectionModule } from './modules/KeyValueDetectionModule'; +import { LinesToParagraphModule } from './modules/LinesToParagraphModule'; +import { LinkDetectionModule } from './modules/LinkDetectionModule'; +import { Module } from './modules/Module'; +import { NumberCorrectionModule } from './modules/NumberCorrectionModule'; +import { OutOfPageRemovalModule } from './modules/OutOfPageRemovalModule'; +import { ReadingOrderDetectionModule } from './modules/ReadingOrderDetectionModule'; +import { RedundancyDetectionModule } from './modules/RedundancyDetectionModule'; +import { RegexMatcherModule } from './modules/RegexMatcherModule'; +import { RemoteModule } from './modules/RemoteModule'; +import { SeparateWordsModule } from './modules/SeparateWordsModule'; +import { WhitespaceRemovalModule } from './modules/WhitespaceRemovalModule'; +import { WordsToLineModule } from './modules/WordsToLineModule'; +import { CleanerConfig, Config } from './types/Config'; +import { Document } from './types/DocumentRepresentation/Document'; +import logger from './utils/Logger'; + +/** + * The cleaner a pipeline of tool used to clean up the PDF file represented in the Json, + * such as removing useless white blocks, merging text blocks together to form words, sentences or paragraphs. + * It can also add some metadata to any block to help higher level tools. + */ +export class Cleaner { + private modules: Module[] = []; + private solvedDependencies: Array = []; + + // Every newly created module should figure in this register + private cleaningToolRegister: Array = [ + OutOfPageRemovalModule, + ReadingOrderDetectionModule, + WordsToLineModule, + KeyValueDetectionModule, + LinesToParagraphModule, + HierarchyDetectionModule, + LinkDetectionModule, + HeaderFooterDetectionModule, + NumberCorrectionModule, + RedundancyDetectionModule, + WhitespaceRemovalModule, + HeadingDetectionModule, + RegexMatcherModule, + RemoteModule, + SeparateWordsModule, + // Add your own module here! + ]; + + /** + * Constructor for a cleaner based class. + * + * @param config Configuration for the cleaner type module. + * @remarks Sets up the cleaner module with the configuration passed, along with checking if the dependencies. + */ + constructor(config: Config) { + if (config.version <= 0.4) { + this.parse0_4Config(config.cleaner); + } else { + this.parseLatestConfig(config.cleaner); + } + } + + /** + * Get a module using just its name. + * + * @param config Configuration for the cleaner type module. + * @returns The found module. + * @remarks Sets up the cleaner module with the configuration passed, along with checking if the dependencies. + */ + public getModuleByName(name: string): typeof Module { + const moduleClass: typeof Module = this.cleaningToolRegister.filter( + M => M.moduleName === name, + )[0]; + if (!moduleClass) { + throw new Error( + `Module called ${name} not found. Please check your config file with the documentation.`, + ); + } + + return moduleClass; + } + + /** + * Runs the cleaning pipeline. + * + * @param document The document to be cleaned + * @returns The promise of the document after the run of all the cleaning modules. + * @remarks Goes through all the modules one by one and executes them, noting + * the execution time for each one, then logging it. + */ + public run(document: Document): Promise { + const startTime: number = Date.now(); + return this.runNextModule(document, 0).then(newDocument => { + const endTime: number = (Date.now() - startTime) / 1000; + logger.info(`Total elapsed time: ${endTime}s`); + return newDocument; + }); + } + + private parseLatestConfig(config: CleanerConfig) { + config.forEach((entry: string | [string, object]) => { + let toolName: string; + let options: object = {}; + + if (Array.isArray(entry)) { + toolName = entry[0]; + + if (typeof entry[1] === 'object') { + options = entry[1]; + } + } else { + toolName = entry; + } + + const moduleClass: typeof Module = this.getModuleByName(toolName); + this.checkDependenciesAndAdd(moduleClass); + this.modules.push(new moduleClass(options)); + }); + } + + private parse0_4Config(config: CleanerConfig) { + for (let i = 0; i < config.length; i++) { + const toolName = config[i]; + if (typeof toolName !== 'string') { + throw new Error(`expected tool name as string instead of options as object at index ${i}`); + } + + let options: object = {}; + + const opt = config[i + 1]; + if (i + 1 < config.length && typeof opt === 'object') { + options = opt; + i++; + } + + const moduleClass: typeof Module = this.getModuleByName(toolName); + this.checkDependenciesAndAdd(moduleClass); + this.modules.push(new moduleClass(options)); + } + } + + /** + * Get a module using just its name. + * + * @param document The document on which the module is to be run. + * @param i The index of the module to be run (among a list of modules). + * @returns The promise of the document after running the next module. + * @remarks Sets up the cleaner module with the configuration passed, along with checking of the dependencies. + */ + private runNextModule(document: Document, i: number): Promise { + if (i < this.modules.length) { + logger.info( + `Running module: ${this.modules[i].constructor.name}, Options: ${JSON.stringify( + this.modules[i].options, + )}`, + ); + + const startTime: number = Date.now(); + return this.modules[i].run(document).then((doc: Document) => { + const endTime: number = (Date.now() - startTime) / 1000; + logger.info(` Elapsed time: ${endTime}s`); + return this.runNextModule(doc, i + 1); + }); + } else { + return Promise.resolve(document); + } + } + + /** + * Check for dependencies given a type of a module, and then add it + * + * @param moduleClass The type of a module to be checked for. + */ + private checkDependenciesAndAdd(moduleClass: typeof Module) { + const unresolved: Array = []; + let isCovered: boolean = true; + + moduleClass.dependencies.forEach(dependency => { + if (!this.solvedDependencies.includes(dependency)) { + isCovered = false; + unresolved.push(dependency); + } + }); + + if (isCovered) { + this.solvedDependencies.push(moduleClass); + } else { + const unresolvedStr: string = unresolved.map(m => m.moduleName).join(', '); + throw new Error( + `Module ${moduleClass.moduleName} has unresolved dependencies (${unresolvedStr}).`, + ); + } + } +} diff --git a/server/src/Orchestrator.ts b/server/src/Orchestrator.ts new file mode 100644 index 00000000..9e45131c --- /dev/null +++ b/server/src/Orchestrator.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Cleaner } from './Cleaner'; +import { Extractor } from './extractors/Extractor'; +import { Document } from './types/DocumentRepresentation'; +import logger from './utils/Logger'; + +/** + * The orchestrator class handles the various steps which a document goes through, including extraction, cleaning. + * This class serves as the base class for specific instances of the class to be generated + * using particular extractors and cleaners. + */ +export class Orchestrator { + public extractor: Extractor; + public cleaner: Cleaner; + + /** + * Constructs the orchestrator object with a specific extractor + * + * @param extractor The choice of the extractor to be used. To be chosen among abbyy, tesseract, pdf2json, etc. + * @param cleaner The cleaner module specifies the handler for all cleaning tasks in the second + * phase of the doc treatment. + */ + constructor(extractor: Extractor, cleaner: Cleaner) { + this.extractor = extractor; + this.cleaner = cleaner; + } + + /** + * Runs the orchestrator, performing first the extraction, then the cleaner, followed + * + * @param document The document on which the module is to be run. + * @param i The index of the module to be run (among a list of modules). + * @returns The promise of the document after running the next module. + * @remarks Sets up the cleaner module with the configuration passed, along with checking of the dependencies. + */ + public run(filename: string): Promise { + logger.info(`Using extractor: ${this.extractor.constructor.name}`); + + return this.extractor.run(filename).then((doc: Document) => { + logger.info('Running cleaner...'); + return this.cleaner.run(doc); + }); + } +} diff --git a/server/src/exporters/ConfidencesExporter.ts b/server/src/exporters/ConfidencesExporter.ts new file mode 100644 index 00000000..067d8fce --- /dev/null +++ b/server/src/exporters/ConfidencesExporter.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Exporter } from './Exporter'; + +export class ConfidencesExporter extends Exporter { + public export(): Promise { + throw new Error('Not implemented yet.'); // TODO + } +} diff --git a/server/src/exporters/CsvExporter.ts b/server/src/exporters/CsvExporter.ts new file mode 100644 index 00000000..7bbb4351 --- /dev/null +++ b/server/src/exporters/CsvExporter.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as stringify from 'csv-stringify/lib/sync'; +import * as fs from 'fs'; +import * as path from 'path'; + +import { Page, Table } from '../types/DocumentRepresentation'; +import { Exporter } from './Exporter'; + +export class CsvExporter extends Exporter { + public export(outputPath: string): Promise { + const promises: Array> = []; + const ext = '.csv'; + + // FIXME This is a dirty way to check if the folder already exists + try { + fs.mkdirSync(`${path.dirname(outputPath)}/csv`); + } catch { + // noop + } + + outputPath = `${path.dirname(outputPath)}/csv/${path.basename(outputPath, ext)}`; + + this.doc.pages.forEach((page: Page) => { + let index = 1; + + page.elements + .filter(e => e instanceof Table) + .forEach((table: Table) => { + promises.push( + this.writeFile( + `${outputPath}-${page.pageNumber}-${index}${ext}`, + this.getCsvContent(table), + ), + ); + index++; + }); + }); + + return Promise.all(promises); + } + + private getCsvContent(table: Table): string { + return stringify(table.toArray(), { delimiter: ';' }); + } +} diff --git a/server/src/exporters/Exporter.ts b/server/src/exporters/Exporter.ts new file mode 100644 index 00000000..d7393930 --- /dev/null +++ b/server/src/exporters/Exporter.ts @@ -0,0 +1,43 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import { Document } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; + +export abstract class Exporter { + public doc: Document; + + constructor(doc: Document) { + this.doc = doc; + } + + public abstract export(outputPath: string): Promise; + + protected writeFile( + outputPath: string, + content: string, + encoding: string = 'utf8', + ): Promise { + return new Promise(resolve => { + logger.info(`Writing file: ${outputPath}`); + fs.writeFileSync(outputPath, content, encoding); + // Check that the file is correctly written on the file system + fs.fsyncSync(fs.openSync(outputPath, 'r+')); + resolve(); + }); + } +} diff --git a/server/src/exporters/JsonCompactExporter.ts b/server/src/exporters/JsonCompactExporter.ts new file mode 100644 index 00000000..ba26b9f5 --- /dev/null +++ b/server/src/exporters/JsonCompactExporter.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Exporter } from './Exporter'; + +export class JsonCompactExporter extends Exporter { + public export(): Promise { + throw new Error('Not implemented yet.'); // TODO + } +} diff --git a/server/src/exporters/JsonExporter.ts b/server/src/exporters/JsonExporter.ts new file mode 100644 index 00000000..9ee6d49f --- /dev/null +++ b/server/src/exporters/JsonExporter.ts @@ -0,0 +1,273 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Barcode, + BoundingBox, + Character, + Document, + Drawing, + Element, + Font, + Heading, + Image, + JsonBox, + JsonElement, + JsonExport, + JsonFont, + JsonMetadata, + JsonPage, + List, + Page, + Table, + TableCell, + TableRow, + Text, + Word, +} from '../types/DocumentRepresentation'; +import { SvgLine } from '../types/DocumentRepresentation/SvgLine'; +import { SvgShape } from '../types/DocumentRepresentation/SvgShape'; +import { ComplexMetadata, Metadata, NumberMetadata } from '../types/Metadata'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; +import { Exporter } from './Exporter'; + +export class JsonExporter extends Exporter { + private granularity: string; + private currentMetadataId: number = 1; + private currentFontId: number = 1; + private json: JsonExport = {} as JsonExport; + private fontCatalog: Map = new Map(); + private metadataCatalog: Map = new Map(); + + constructor(doc: Document, granularity: string) { + super(doc); + this.granularity = granularity; + } + + public export(outputPath: string): Promise { + logger.info('Exporting json...'); + return this.writeFile(outputPath, JSON.stringify(this.getJson())); + } + + public getJson(): JsonExport { + this.json.metadata = []; + + this.buildMetadataCatalog(); + this.metadataToJson(); + + this.json.pages = this.doc.pages.map((page: Page) => { + const jsonPage: JsonPage = { + box: this.boxToJsonBox(page.box), + pageNumber: page.pageNumber, + elements: page.elements + .sort(utils.sortElementsByOrder) + .map((element: Element) => this.elementToJsonElement(element)), + }; + + return jsonPage; + }); + + this.json.fonts = []; + this.fontCatalog.forEach((fontId, font) => { + const name = font.name; + const size = font.size; + const url = font.url; + const scaling = font.scaling; + let weight = font.weight; + let isItalic = font.isItalic; + let isUnderline = font.isUnderline; + let color = font.color; + + if (font.color !== 'black' && font.color !== '#000000' && font.color !== '000000') { + color = font.color; + } + + if (font.weight !== 'medium') { + weight = font.weight; + } + + if (font.isItalic) { + isItalic = font.isItalic; + } + + if (font.isUnderline) { + isUnderline = font.isUnderline; + } + + const jsonFont: JsonFont = { + id: fontId, + name, + size, + weight, + isItalic, + isUnderline, + color, + url, + scaling, + }; + + this.json.fonts.push(jsonFont); + }); + + return this.json; + } + + private buildMetadataCatalog() { + // Build the catalog, avoiding duplicates thanks to the Map + this.doc.pages.forEach(page => { + page.elements.forEach(elem => { + this.findMetadata(elem); + }); + }); + } + + private findMetadata(element: Element) { + element.metadata.forEach(m => { + if (!this.metadataCatalog.has(m)) { + this.metadataCatalog.set(m, this.currentMetadataId++); + } + }); + + if (element.content instanceof Array) { + element.content.forEach(elem => this.findMetadata(elem)); + } + } + + private metadataToJson() { + this.metadataCatalog.forEach((id: number, metadata: Metadata) => { + const jsonMetadata: JsonMetadata = { + id, + elements: metadata.elements.map(e => e.id).sort((a, b) => a - b), + type: utils.toKebabCase(metadata.constructor.name).replace(/-metadata$/, ''), + }; + + if (metadata instanceof NumberMetadata) { + jsonMetadata.value = metadata.value; + } + + if (metadata instanceof ComplexMetadata) { + Object.keys(metadata.data).forEach(k => { + metadata.data[k] = this.convertElementValue(metadata.data[k]); + }); + + jsonMetadata.data = metadata.data; + } + + this.json.metadata.push(jsonMetadata); + }); + } + + private elementToJsonElement(element: Element) { + const id: number = element.id; + const type: string = utils.toKebabCase(element.constructor.name); + + const jsonElement: JsonElement = { + id, + type, + }; + + if (element.properties) { + jsonElement.properties = element.properties; + } + + jsonElement.metadata = element.metadata.map(metadata => this.metadataCatalog.get(metadata)); + + if (Element.hasBoundingBox(element)) { + jsonElement.box = this.boxToJsonBox(element.box); + } + + if (element instanceof Text) { + jsonElement.conf = element.confidence; + + if (typeof element.content === 'string') { + jsonElement.content = element.content; + } else if ( + this.granularity === 'word' && + element.content instanceof Array && + element.content.length > 0 && + element.content[0] instanceof Character + ) { + jsonElement.content = element.toString(); + } else { + jsonElement.content = element.content + .sort(utils.sortElementsByOrder) + .map(elem => this.elementToJsonElement(elem)); + } + + if (element instanceof Word) { + if (typeof element.font !== 'undefined') { + if (!this.fontCatalog.has(element.font)) { + this.fontCatalog.set(element.font, this.currentFontId++); + } + + jsonElement.font = this.fontCatalog.get(element.font); + } + } + } else if (element instanceof List) { + jsonElement.isOrdered = element.isOrdered; + jsonElement.content = element.content.map(elem => this.elementToJsonElement(elem)); + } else if (element instanceof Table) { + jsonElement.content = element.content.map(elem => this.elementToJsonElement(elem)); + } else if (element instanceof TableRow) { + jsonElement.content = element.content.map(elem => this.elementToJsonElement(elem)); + } else if (element instanceof TableCell) { + jsonElement.rowspan = element.rowspan; + jsonElement.colspan = element.colspan; + jsonElement.content = element.content.map(elem => this.elementToJsonElement(elem)); + } else if (element instanceof SvgShape) { + if (element instanceof SvgLine) { + jsonElement.fromX = element.fromX; + jsonElement.fromY = element.fromY; + jsonElement.toX = element.toX; + jsonElement.toY = element.toY; + jsonElement.thickness = element.thickness; + } + } else if (element instanceof Drawing) { + jsonElement.content = element.content.map(elem => this.elementToJsonElement(elem)); + } else if (element instanceof Barcode) { + jsonElement.codeType = element.type; + jsonElement.codeValue = element.content; + } else if (element instanceof Image) { + jsonElement.url = element.url; + } else if (element instanceof Heading) { + jsonElement.level = element.level; + } + + return jsonElement; + } + + private boxToJsonBox(box: BoundingBox): JsonBox { + const jsonBox: JsonBox = { + l: utils.round(box.left), + t: utils.round(box.top), + w: utils.round(box.width), + h: utils.round(box.height), + }; + + return jsonBox; + } + + private convertElementValue(value: any): any { + if (value instanceof Element) { + return value.id; + } else if (value instanceof Array) { + return value.map(v => this.convertElementValue(v)); + } else { + return value; + } + } +} diff --git a/server/src/exporters/MarkdownExporter.ts b/server/src/exporters/MarkdownExporter.ts new file mode 100644 index 00000000..d4cfc4aa --- /dev/null +++ b/server/src/exporters/MarkdownExporter.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as mdtable from 'markdown-table'; +import { Document, Heading, List, Paragraph, Table } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; +import { Exporter } from './Exporter'; + +export class MarkdownExporter extends Exporter { + private includeHeaderFooter: boolean; + + constructor(doc: Document, includeHeaderFooter: boolean) { + super(doc); + this.includeHeaderFooter = includeHeaderFooter; + } + + public export(outputPath: string): Promise { + logger.info('Exporting markdown...'); + return this.writeFile(outputPath, this.getMarkdown()); + } + + private getMarkdown(): string { + let output: string = ''; + this.doc.pages.forEach(page => { + page.elements.forEach(element => { + if ( + (element.properties.isHeader || element.properties.isFooter) && + !this.includeHeaderFooter + ) { + return; + } + if (element instanceof Heading) { + if (element.level < 7) { + let theHeading: string = ''; + for (let i = 0; i !== element.level; ++i) { + theHeading += '#'; + } + theHeading += ' '; + theHeading += element.toString().replace(/(?:\r\n|\r|\n)/g, '
'); + output += theHeading; + output += '\n'.repeat(2); + } else { + output += element.toString(); + output += '\n'.repeat(2); + } + } else if (element instanceof Paragraph) { + output += element.toString(); + output += '\n'.repeat(2); + } else if (element instanceof List) { + element.content.forEach((para, itemNumber) => { + const paraText: string = para.toString(); + if (element.isOrdered) { + output += (itemNumber + 1).toString() + ' '; + } else { + output += '- '; + } + output += paraText; + output += '\n'; + }); + } else if (element instanceof Table) { + output += mdtable(element.toArray()); + output += '\n'.repeat(2); + } + }); + // end of page + output += '\n'.repeat(10); + }); + return output; + } +} diff --git a/server/src/exporters/PdfExporter.ts b/server/src/exporters/PdfExporter.ts new file mode 100644 index 00000000..0770f144 --- /dev/null +++ b/server/src/exporters/PdfExporter.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import * as os from 'os'; +import { Document } from '../types/DocumentRepresentation'; +import { getTemporaryFile } from '../utils'; +import logger from '../utils/Logger'; +import { Exporter } from './Exporter'; +import { MarkdownExporter } from './MarkdownExporter'; + +export class PdfExporter extends Exporter { + private includeHeaderFooter: boolean; + + constructor(doc: Document, includeHeaderFooter: boolean) { + super(doc); + this.includeHeaderFooter = includeHeaderFooter; + } + + public export(outputPath: string): Promise { + const markdownFilename: string = getTemporaryFile('.md'); + const markdownExporter = new MarkdownExporter(this.doc, this.includeHeaderFooter); + markdownExporter.export(markdownFilename).then(() => { + const pandocPath = spawnSync('which', ['pandoc']).output.join(''); + if (pandocPath === '' || (/^win/i.test(os.platform()) && /no pandoc in/.test(pandocPath))) { + logger.warn('Pandoc not installed !! Skip PDF export.'); + return Promise.reject(); + } else { + const pandocSync = spawnSync( + 'pandoc', + [ + '-f', + 'markdown_github+all_symbols_escapable', + '--pdf-engine=xelatex', + '--quiet', + '-s', + markdownFilename, + '-o', + outputPath, + ], + { + cwd: process.cwd(), + env: process.env, + }, + ); + if (pandocSync.status === 0) { + logger.info(`Writing file: ${outputPath}`); + return Promise.resolve(pandocSync.status); + } else { + logger.error(`Error writing PDF file ${outputPath}`); + return Promise.reject(pandocSync.status); + } + } + }); + return Promise.resolve(); + } +} diff --git a/server/src/exporters/TextExporter.ts b/server/src/exporters/TextExporter.ts new file mode 100644 index 00000000..a9db93d5 --- /dev/null +++ b/server/src/exporters/TextExporter.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Paragraph, Table } from '../types/DocumentRepresentation'; +import { Exporter } from './Exporter'; + +export class TextExporter extends Exporter { + private includeHeaderFooter: boolean; + + constructor(doc: Document, includeHeaderFooter: boolean) { + super(doc); + this.includeHeaderFooter = includeHeaderFooter; + } + + public export(outputPath: string): Promise { + return this.writeFile(outputPath, this.getPlainText()); + } + + private getPlainText(): string { + let output: string = ''; + this.doc.pages.forEach(page => { + page.elements.forEach(element => { + if ( + (element.properties.isHeader || element.properties.isFooter) && + !this.includeHeaderFooter + ) { + return; + } + if (element instanceof Paragraph) { + output = output.concat(element.toString()); + } else if (element instanceof Table) { + element.content.forEach(tableRow => { + tableRow.content.forEach(tableCell => { + tableCell.content.forEach(para => { + const paraContent: string = para.toString().replace(/\n/, ''); + output = output.concat(paraContent); + // output = output.concat('\t'); + }); + output = output.concat('\t'); + }); + output = output.concat('\n'); + }); + output = output.concat('\n\n'); + } + // end of paragraph + output = output.concat('\n\n'); + }); + // end of page + output = output.concat('\n'.repeat(10)); + }); + return output; + } +} diff --git a/server/src/exporters/XmlExporter.ts b/server/src/exporters/XmlExporter.ts new file mode 100644 index 00000000..6f2e17f1 --- /dev/null +++ b/server/src/exporters/XmlExporter.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Exporter } from './Exporter'; + +export class XmlExporter extends Exporter { + public export(): Promise { + throw new Error('Not implemented yet.'); // TODO + } +} diff --git a/server/src/exporters/index.ts b/server/src/exporters/index.ts new file mode 100644 index 00000000..aa5b4170 --- /dev/null +++ b/server/src/exporters/index.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Exporter'; +export * from './JsonExporter'; +export * from './JsonCompactExporter'; +export * from './MarkdownExporter'; +export * from './TextExporter'; +export * from './XmlExporter'; +export * from './ConfidencesExporter'; +export * from './CsvExporter'; +export * from './PdfExporter'; diff --git a/server/src/extractors/Extractor.ts b/server/src/extractors/Extractor.ts new file mode 100644 index 00000000..395b3071 --- /dev/null +++ b/server/src/extractors/Extractor.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '../types/Config'; +import { Document } from '../types/DocumentRepresentation/Document'; + +/** + * The extractor is responsible to extract every possible information + * from the PDF/Image file and store it in the Json file. + * It also ensure that the Json file is correctly formated and contains all the needed + * information in a clever way. + */ +export abstract class Extractor { + public config: Config; + + constructor(config: Config) { + this.config = config; + } + + public abstract run(inputFile: string): Promise; +} diff --git a/server/src/extractors/abbyy/AbbyyClient.ts b/server/src/extractors/abbyy/AbbyyClient.ts new file mode 100644 index 00000000..251de64a --- /dev/null +++ b/server/src/extractors/abbyy/AbbyyClient.ts @@ -0,0 +1,362 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as axios from 'axios'; +import { readFileSync, writeFileSync } from 'fs'; +import * as path from 'path'; +import { parseString } from 'xml2js'; +import * as utils from '../../utils'; +import logger from '../../utils/Logger'; + +export class AbbyyClient { + /** + * Getter serverTimeout + * @return {number} + */ + public get serverTimeout(): number { + return this._serverTimeout; + } + + /** + * Setter serverTimeout + * @param {number} value + */ + public set serverTimeout(value: number) { + this._serverTimeout = value; + } + + /** + * Getter host + * @return {string} + */ + public get host(): string { + return this._host; + } + + /** + * Setter host + * @param {string} value + */ + public set host(value: string) { + this._host = value; + } + + /** + * Getter serverVersion + * @return {string} + */ + public get serverVersion(): string { + return this._serverVersion; + } + + /** + * Setter serverVersion + * @param {string} value + */ + public set serverVersion(value: string) { + this._serverVersion = value; + } + + /** + * Getter serverString + * @return {string} + */ + public get serverString(): string { + return this._serverString; + } + + /** + * Setter serverString + * @param {string} value + */ + public set serverString(value: string) { + this._serverString = value; + } + + /** + * Getter workflowList + * @return {string[]} + */ + public get workflowList(): string[] { + return this._workflowList; + } + + /** + * Setter workflowList + * @param {string[]} value + */ + public set workflowList(value: string[]) { + this._workflowList = value; + } + + /** + * Getter headers + * @return {object} + */ + public get headers(): object { + return this._headers; + } + + /** + * Setter headers + * @param {object} value + */ + public set headers(value: object) { + this._headers = value; + } + + public reqStrGetWorkflows: string = ` + + + + localhost + + + `; + + public reqStrFileSend: string = ` + + + + localhost + PLACEHOLDER_WORKFLOW + + PLACEHOLDER_FILENAME + PLACEHOLDER_FILECONTENT + + + + `; + + public reqStrGetJobState: string = ` + + + + localhost + PLACEHOLDER_JOBID + + + `; + + public reqStrGetResult: string = ` + + + + localhost + PLACEHOLDER_JOBID + DoNotDeleteJob + + + `; + private _host: string; + private _serverVersion: string; + private _serverString: string; + private _workflowList: string[]; + private _headers: object; + private _serverTimeout: number; + + constructor(host: string, serverVersion: string, serverTimeout: number) { + this.host = host; + this.serverVersion = serverVersion; + this.serverString = `http://${this.host}/FineReaderServer${this.serverVersion}/WebService.asmx`; + this.serverTimeout = serverTimeout; + this.headers = { + 'content-type': 'text/xml', + }; + } + + public run( + workflowName: string, + filename: string, + pollingInterval: number = 1000, + ): Promise { + const promise = new Promise((resolve, reject) => { + this.getWorkflows() + .then(workFlows => { + logger.info('[AbbyyClient getWorkflows]: got these workflows:', workFlows); + logger.info( + `[AbbyyClient send]: sending file: ${filename} on the workflow: ${workflowName}`, + ); + return this.sendFile(filename, workflowName); + }) + .then(jobId => { + logger.info('[AbbyyClient send]: got this jobId:', jobId); + return this.waitTillJobDone(jobId, pollingInterval); + }) + .then(jobId => { + logger.info('[AbbyyClient jobStatus]: Finished, getting result.'); + return this.getResult(jobId); + }) + .then(xml => { + logger.info('[AbbyyClient result]: Returning xml result.'); + resolve(xml); + }) + .catch(err => { + logger.error('[AbbyyClient jobResult]: got an error:', err); + reject(err); + }); + }); + return promise; + } + + public soapRequest(url: string, headers: any, xml: any, timeout = 0x7fffffff): Promise { + const config: axios.AxiosRequestConfig = { + method: 'post', + url, + headers, + data: xml, + timeout, + maxContentLength: Infinity, + proxy: false, + }; + + const promise: Promise = axios + .default(config) + .then(response => { + if (response.status !== 200) { + throw new Error(`Unexpected response code ${response.status}, ${response.data}`); + } + + return response; + }) + .then(response => { + return this.parseResponse(response); + }); + + return promise; + } + + private parseResponse(response: axios.AxiosResponse): Promise { + return new Promise((resolve, reject) => { + parseString(response.data, (err: any, obj: any) => { + if (err) { + reject(err); + } + + resolve(obj); + }); + }); + } + + private getWorkflows(): Promise { + const promise: Promise = new Promise(resolve => { + this.soapRequest( + this.serverString, + this.headers, + this.reqStrGetWorkflows, + this.serverTimeout, + ).then((obj: any) => { + this.workflowList = + obj['soap:Envelope']['soap:Body'][0].GetWorkflowsResponse[0].GetWorkflowsResult[0].string; + resolve(this.workflowList); + }); + }); + + return promise; + } + + private sendFile(filename: string, workflowName: string): Promise { + const filenameOnly = path.parse(filename).base; + logger.info('Sending the file ', filenameOnly, 'on workflow', workflowName); + + const promise = new Promise((resolve, reject) => { + if (!this.workflowList.includes(workflowName)) { + return reject('Workflow not found'); + } + + const fileContent: string = readFileSync(filename, 'base64'); + let sendString = this.reqStrFileSend.replace('PLACEHOLDER_WORKFLOW', workflowName); + sendString = sendString.replace('PLACEHOLDER_FILENAME', filenameOnly); + sendString = sendString.replace('PLACEHOLDER_FILECONTENT', fileContent); + + this.soapRequest(this.serverString, this.headers, sendString, this.serverTimeout).then( + obj => { + resolve( + obj['soap:Envelope']['soap:Body'][0].StartProcessFileResponse[0] + .StartProcessFileResult[0], + ); + }, + ); + }); + + return promise; + } + + private waitTillJobDone(jobId: string, wait: number): Promise { + const promise = new Promise(resolve => { + this.soapRequest( + this.serverString, + this.headers, + this.reqStrGetJobState.replace('PLACEHOLDER_JOBID', jobId), + ).then(obj => { + logger.debug('current state of job', jobId, 'is', utils.prettifyObject(obj)); + const state: any = + obj['soap:Envelope']['soap:Body'][0].GetJobStateInfoResponse[0].GetJobStateInfoResult[0]; + + logger.info( + `[AbbyyClient jobStatus]: Job ${jobId}: ${state.State[0]}, ${state.Progress[0]}`, + ); + + if (state.State[0] === 'JS_Complete') { + resolve(jobId); + } else { + resolve(this.waitTillJobDone(jobId, wait)); + } + }); + }); + + return promise; + } + + private getResult(jobId: string): Promise { + const promise = new Promise(resolve => { + this.soapRequest( + this.serverString, + this.headers, + this.reqStrGetResult.replace('PLACEHOLDER_JOBID', jobId), + this.serverTimeout, + ).then(obj => { + const docs: { [key: string]: any } = + obj['soap:Envelope']['soap:Body'][0].GetJobResultExResponse[0].GetJobResultExResult[0] + .JobDocuments[0].JobDocument[0].OutputDocuments[0].OutputDocument; + for (const docKey in docs) { + const doc = docs[docKey]; + if (doc.FileFormat[0] === 'OFF_XML') { + const fileName: string = path.resolve( + utils.getTemporaryDirectory() + '/' + doc.Files[0].FileContainer[0].FileName[0], + ); + const fileContentBuffer: string = doc.Files[0].FileContainer[0].FileContents[0]; + const fileContent: string = Buffer.from(fileContentBuffer, 'base64').toString('utf8'); + + writeFileSync(fileName, fileContent, { encoding: 'utf-8' }); + logger.info('ABBYY XML file written to', fileName); + resolve(fileContent); + } + } + }); + }); + + return promise; + } +} diff --git a/server/src/extractors/abbyy/AbbyyTools.ts b/server/src/extractors/abbyy/AbbyyTools.ts new file mode 100644 index 00000000..cbfd8b89 --- /dev/null +++ b/server/src/extractors/abbyy/AbbyyTools.ts @@ -0,0 +1,456 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseString } from 'xml2js'; +import { + Barcode, + BoundingBox, + Character, + Drawing, + Element, + Font, + Image, + Line, + List, + Page, + Paragraph, + Table, + TableCell, + TableRow, + Word, +} from '../../types/DocumentRepresentation'; +import { Document } from '../../types/DocumentRepresentation/Document'; +import { SvgLine } from '../../types/DocumentRepresentation/SvgLine'; +import { SvgShape } from '../../types/DocumentRepresentation/SvgShape'; +import * as utils from '../../utils'; +import logger from '../../utils/Logger'; +import { Extractor } from '../Extractor'; +import { AbbyyClient } from './AbbyyClient'; + +export class AbbyyTools extends Extractor { + /** + * Getter fonts + * @return {Font[] } + */ + public get fonts(): Font[] { + return this._fonts; + } + + /** + * Setter fonts + * @param {Font[] } value + */ + public set fonts(value: Font[]) { + this._fonts = value; + } + private _fonts: Font[] = []; + + public abbyyXMLToObject(xml: string): Promise { + const promise = new Promise((resolve, reject) => { + parseString(xml, (err, dataObject) => { + if (err) { + reject(err); + } + resolve(dataObject); + }); + }); + return promise; + } + + public run(inputFile: string): Promise { + const host: string = process.env.ABBYY_SERVER_URL || '10.0.0.252'; + const serverVersion: string = process.env.ABBYY_SERVER_VER || '14'; + const workflowName: string = process.env.ABBYY_WORKFLOW || 'workflow-hotfolder-d_drive'; + const serverTimeout: number = 50000; + const jobPollingInterval: number = 1000; + + const client: AbbyyClient = new AbbyyClient(host, serverVersion, serverTimeout); + const promise: Promise = client + .run(workflowName, inputFile, jobPollingInterval) + .then(xml => this.abbyyXMLToObject(xml)) + .then((obj: any) => { + const doc = obj.document.page; + const promises: Array> = []; + + for (const pageNumber in doc) { + const pageObj = doc[pageNumber]; + // the last argument signifies that blade lines + // are calculated automatically for each page + promises.push(this.parseAbbyyPage(pageObj, parseInt(pageNumber, 10))); + } + + return Promise.all(promises); + }) + .then((pages: Page[]) => new Document(pages)) + .catch(err => { + throw new Error( + `There was an error while interfacing with the ABBYY Finereader server: ${err}`, + ); + }); + + return promise; + } + + protected parseAbbyyPage(pageObject: any, pageNumber: number): Promise { + const promise = new Promise(async (resolve, reject) => { + let elements: Element[] = []; + try { + for (const blockKey in pageObject.block) { + const block = pageObject.block[blockKey]; + elements = elements.concat(this.parseBlock(block)); + } + } catch (err) { + reject(err); + } + const pageWidth = parseInt(pageObject.$.width, 10); + const pageHeight = parseInt(pageObject.$.height, 10); + const page = new Page(pageNumber + 1, elements, new BoundingBox(0, 0, pageWidth, pageHeight)); + resolve(page); + }); + return promise; + } + + private computeBoundingBox(abbyyBoxAttribs: any): BoundingBox { + const left = parseInt(abbyyBoxAttribs.l, 10); + const right = parseInt(abbyyBoxAttribs.r, 10); + const top = parseInt(abbyyBoxAttribs.t, 10); + const bottom = parseInt(abbyyBoxAttribs.b, 10); + + return new BoundingBox(left, top, right - left, bottom - top); + } + + // tslint:disable: no-bitwise + private abbyyColorToRgbHex(bgr: number): string { + const r: number = (bgr >> 16) & 255; + const g: number = (bgr >> 8) & 255; + const b: number = bgr & 255; + const rgb: number = (b << 16) | (g << 8) | r; + return rgb.toString(16).toUpperCase(); + } + + private newFontProcess(newFont: Font): Font { + const existingFonts: Font[] = this.fonts.filter(font => font.isEqual(newFont)); + if (existingFonts.length !== 0) { + return existingFonts[0]; + } else { + this.fonts.push(newFont); + return newFont; + } + } + + private fontFromFormatting(formattingObj: any): Font { + let ff: string = ''; + let fs: number = 0; + + const fontoptions: any = {}; + if ('bold' in formattingObj) { + if (formattingObj.bold === '1') { + fontoptions.weight = 'bold'; + } + } + if ('italic' in formattingObj) { + fontoptions.italic = formattingObj.italic === '1'; + } + if ('underline' in formattingObj) { + fontoptions.underline = formattingObj.underline === '1'; + } + if ('color' in formattingObj) { + fontoptions.color = this.abbyyColorToRgbHex(parseInt(formattingObj.color, 10)); + } + if ('scaling' in formattingObj) { + fontoptions.scaling = parseInt(formattingObj.scaling, 10); + } + if ('ff' in formattingObj) { + ff = formattingObj.ff; + } + if ('fs' in formattingObj) { + fs = parseInt(formattingObj.fs, 10); + } + + const font: Font = new Font(ff, fs, fontoptions); + return this.newFontProcess(font); + } + + private parseAbbyyParagraph(para: any): Paragraph { + const linesDS: Line[] = []; + for (const lineNum in para.line) { + const line = para.line[lineNum]; + let wordsConcat: string = ''; // DS for words if no letter level details + const wordsDS: Word[] = []; + for (const thisLineKey in line.formatting) { + const lineContent = line.formatting[thisLineKey]; + const font = this.fontFromFormatting(lineContent.$); + if ('charParams' in lineContent) { + // means that char level data was generated + let charsDS: Character[] = []; + const characterObjs = lineContent.charParams; + let isInDictionary: boolean = false; + for (const thisCharKey in characterObjs) { + const thisChar = characterObjs[thisCharKey]; + const charBbox: BoundingBox = this.computeBoundingBox(thisChar.$); + if (charBbox.areaIsEmpty()) { + // if it has some content, it should be dealt with + continue; + } + if (typeof thisChar._ === 'undefined') { + // if ('isTab' in thisChar['$']) { // TODO: use this tab/space information somehow + // charVal = "\t" + // } + // else { + // charVal = " " + // } + if (charsDS.length !== 0) { + wordsDS.push(this.wordFromCharacters(charsDS, isInDictionary, font)); + charsDS = []; + isInDictionary = false; + } + } else { + const c = new Character(charBbox, thisChar._); + let confidence: number = 1; + if ('charConfidence' in thisChar.$) { + confidence = utils.round(parseInt(thisChar.$.charConfidence, 10) * 0.01); + } + if ('wordFromDictionary' in thisChar.$) { + isInDictionary = thisChar.$.wordFromDictionary === '1'; + } + c.confidence = confidence; + charsDS.push(c); + } + } + if (charsDS.length !== 0) { + wordsDS.push(this.wordFromCharacters(charsDS, isInDictionary, font)); + } + } + if ('_' in lineContent) { + // means that char level data was not generated + const textContent: string = lineContent._; + wordsConcat += textContent; + } + if (wordsConcat !== '') { + wordsConcat.split(/\s+/).forEach(wordAsString => { + if (wordAsString !== '') { + const w: Word = this.wordFromCharacters(wordAsString, false, font); + wordsDS.push(w); + } + }); + } + } + if (wordsDS.length !== 0) { + const l: Line = this.lineFromWords(wordsDS, this.computeBoundingBox(line.$)); + linesDS.push(l); + } + } + return new Paragraph(BoundingBox.merge(linesDS.map(line => line.box)), linesDS); + } + + private wordFromCharacters( + chars: Character[] | string, + isInDictionary: boolean, + font: Font, + ): Word { + let wordDS: Word; + if (typeof chars !== 'string') { + wordDS = new Word(BoundingBox.merge(chars.map(char => char.box)), chars, font); + if (typeof chars[0].confidence !== 'undefined') { + const confidences: number[] = chars.map(char => char.confidence); + wordDS.confidence = utils.round( + confidences.reduce((a, b) => a + b, 0) / confidences.length, + 2, + ); + } + } else { + wordDS = new Word(undefined, chars, font); + } + wordDS.isInDictionary = isInDictionary; + wordDS.font = font; + return wordDS; + } + + private lineFromWords(words: Word[], lineBbox: BoundingBox): Line { + const lineDS: Line = new Line(lineBbox, words); + if (typeof words[0].confidence !== 'undefined') { + const confidences: number[] = words.map(word => word.confidence); + lineDS.confidence = utils.round( + confidences.reduce((a, b) => a + b, 0) / confidences.length, + 2, + ); + } + return lineDS; + } + + private createNewList(paragraph: Paragraph): List { + return new List(paragraph.box, [paragraph], this.treatListItem(paragraph)); + } + + private treatListItem(paragraph: Paragraph): boolean { + let isOrderedList: boolean = false; + if (paragraph.content.length !== 0) { + if (utils.isBullet(paragraph.content[0].content[0])) { + isOrderedList = false; + paragraph.content[0].content.shift(); + } else if (utils.isNumbering(paragraph.content[0].content[0])) { + isOrderedList = true; + } + } + return isOrderedList; + } + + private parseAbbyyTable(tableObject: any, cleanTable: boolean = false): Element[] { + const rowsDS: TableRow[] = []; + const tableBbox: BoundingBox = this.computeBoundingBox(tableObject.$); + let topUntilHere: number = tableBbox.top; + + for (const rowNum in tableObject.row) { + const row = tableObject.row[rowNum]; + const cellsDS: TableCell[] = []; + let leftUntilHere = tableBbox.left; + let maxCellHeight: number = 0; + for (const cellNum in row.cell) { + const cell = row.cell[cellNum]; + const cellWidth = parseInt(cell.$.width, 10); + const cellHeight = parseInt(cell.$.height, 10); + if (maxCellHeight < cellHeight) { + maxCellHeight = cellHeight; + } + let colSpan: number = 1; + if ('colSpan' in cell.$) { + colSpan = parseInt(cell.$.colSpan, 10); + } + let rowSpan: number = 1; + if ('rowSpan' in cell.$) { + rowSpan = parseInt(cell.$.rowSpan, 10); + } + const elementsDS: Element[] = []; + for (const textNum in cell.text) { + // TODO: assumes that the cell content is text. should include all kinds of elements + const text = cell.text[textNum]; + for (const paraNum in text.par) { + const para = text.par[paraNum]; + const paraContent: Paragraph = this.parseAbbyyParagraph(para); + if (paraContent.content.length !== 0 && paraContent.toString() !== '') { + elementsDS.push(paraContent); + } + } + } + const cellBBox: BoundingBox = new BoundingBox( + leftUntilHere, + topUntilHere, + cellWidth, + cellHeight, + ); + const tablecellDS: TableCell = new TableCell(cellBBox, elementsDS, rowSpan, colSpan); + leftUntilHere += cellWidth; + cellsDS.push(tablecellDS); + } + topUntilHere += maxCellHeight; + rowsDS.push(new TableRow(cellsDS, BoundingBox.merge(cellsDS.map(cell => cell.box)))); + } + const tableDS = new Table(rowsDS, tableBbox); + let elements: Element[] = []; + + if (cleanTable) { + elements = tableDS.cleanTable(); + } else { + elements.push(tableDS); + } + return elements; + } + + private parseAbbyyImage(imageObject: any): Image { + return new Image(this.computeBoundingBox(imageObject.$)); + } + + private parseAbbyyDrawing(drawingObject: any): Drawing { + const shapes: SvgShape[] = []; + const separator: any = drawingObject.separator[0]; + const shapeType: string = separator.$.type.trim().toLowerCase(); + const bbox: BoundingBox = this.computeBoundingBox(drawingObject.$); + if (shapeType === 'black' || shapeType === 'dotted') { + const line: SvgLine = new SvgLine( + bbox, + parseFloat(separator.$.thickness), + parseFloat(separator.start['0'].$.x), + parseFloat(separator.start['0'].$.y), + parseFloat(separator.end['0'].$.x), + parseFloat(separator.end['0'].$.y), + ); + shapes.push(line); + } + const d: Drawing = new Drawing(bbox, shapes); + return d; + } + + private parseAbbyyBarcode(barcodeObject: any): Barcode { + const content = barcodeObject.text['0'].par['0'].line['0'].formatting['0']._; + const barcode: Barcode = new Barcode( + this.computeBoundingBox(barcodeObject.$), + barcodeObject.barcodeInfo['0'].$.type, + content, + ); + return barcode; + } + + private parseBlock(block: any, blockType?: string): Element[] { + let elements: Element[] = []; + if (typeof blockType === 'undefined') { + blockType = block.$.blockType.trim().toLowerCase(); + } + if (blockType === 'text') { + let currentList: List = null; + block.text.forEach(t => { + t.par.forEach(p => { + const paragraph: Paragraph = this.parseAbbyyParagraph(p); + const isList: boolean = 'isListItem' in p.$; + + if (isList) { + const listNum: number = parseFloat(p.$.lstNum); + + if (!currentList || listNum === 1) { + currentList = this.createNewList(paragraph); + elements.push(currentList); + } else { + this.treatListItem(paragraph); + currentList.addParagraph(paragraph); + } + } else { + if (paragraph.content.length !== 0 && paragraph.toString() !== '') { + elements.push(paragraph); + } + + currentList = null; + } + }); + }); + } else if (blockType === 'table') { + // the last argument signifies that blade lines + // are calculated automatically for each page + const t: Element[] = this.parseAbbyyTable(block, true); + elements = [...elements, ...t]; + } else if (blockType === 'picture') { + elements.push(this.parseAbbyyImage(block)); + } else if (blockType === 'drawing' || blockType === 'separator') { + const d: Drawing = this.parseAbbyyDrawing(block); + if (typeof d !== 'undefined') { + elements.push(this.parseAbbyyDrawing(block)); + } + } else if (blockType === 'barcode') { + elements.push(this.parseAbbyyBarcode(block)); + } else { + logger.warn('unknown block type:', blockType); + } + return elements; + } +} diff --git a/server/src/extractors/abbyy/AbbyyToolsXml.ts b/server/src/extractors/abbyy/AbbyyToolsXml.ts new file mode 100644 index 00000000..fc7ffd1a --- /dev/null +++ b/server/src/extractors/abbyy/AbbyyToolsXml.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { readFileSync } from 'fs'; +import { Document } from '../../types/DocumentRepresentation'; +import logger from '../../utils/Logger'; +import { AbbyyTools } from './AbbyyTools'; + +export class AbbyyToolsXml extends AbbyyTools { + public run(inputFile: string): Promise { + const promise = new Promise((resolve, reject) => { + const xml: string = readFileSync(inputFile, 'utf-8'); + this.abbyyXMLToObject(xml) + .then((obj: any) => { + const document = new Document(); + try { + const doc = obj.document.page; + for (const pageNumber in doc) { + const pageObj = doc[pageNumber]; + // the last argument signifies that blade lines are calculate automatically + this.parseAbbyyPage(pageObj, parseInt(pageNumber, 10)).then(page => { + document.pages.push(page); + }); + } + } catch (err) { + logger.error('Error during document construction.'); + reject(err); + } + resolve(document); + }) + .catch(err => { + logger.error('There was an error while interfacing with the server:', err); + }); + }); + + return promise; + } +} diff --git a/server/src/extractors/extract-fonts.ts b/server/src/extractors/extract-fonts.ts new file mode 100644 index 00000000..f6cfa810 --- /dev/null +++ b/server/src/extractors/extract-fonts.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; + +// TODO Handle more than just TrueType (.ttf) files +/** + * Stability: Experimental + * Use Mutool to extract fonts files in a specific folder. + */ +export function extractFonts(pdfInputFile: string): Promise { + return new Promise((resolve, reject) => { + const mutoolPath = spawnSync('which', ['mutool']).output.join(''); + if (mutoolPath === '' || (/^win/i.test(os.platform()) && /no mutool in/.test(mutoolPath))) { + logger.warn('MuPDF not installed !! Skip fonts extraction.'); + resolve(); + } else { + logger.info('Extracting fonts...'); + const folder = utils.getMutoolExtractionFolder(); + const command = `mutool extract '${pdfInputFile}'`; + logger.debug(command); + const ret = spawnSync('mutool', ['extract', pdfInputFile], { cwd: folder }); + + if (ret.status !== 0) { + logger.error(ret.stderr.toString()); + reject(ret.stderr.toString()); + } + + const ttfRegExp = /^[A-Z]{6}\+(.*)\-[0-9]+\.ttf$/; + fs.readdirSync(folder).forEach(file => { + const match = file.match(ttfRegExp); + + if (match) { + fs.renameSync(`${folder}/${file}`, `${folder}/${match[1]}` + '.ttf'); + } + }); + + resolve(); + } + }); +} diff --git a/server/src/extractors/json/JsonExtractor.ts b/server/src/extractors/json/JsonExtractor.ts new file mode 100644 index 00000000..e11f66f4 --- /dev/null +++ b/server/src/extractors/json/JsonExtractor.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { readFileSync } from 'fs'; +import { Document, JsonExport } from '../../types/DocumentRepresentation'; +import { json2document } from '../../utils/json2document'; +import logger from '../../utils/Logger'; +import { Extractor } from '../Extractor'; + +export class JsonExtractor extends Extractor { + public run(inputFile: string): Promise { + logger.info('processing the input file', inputFile); + const json: JsonExport = JSON.parse(readFileSync(inputFile, 'utf8')); + const doc: Document = json2document(json); + return Promise.resolve(doc); + } +} diff --git a/server/src/extractors/pdf2json/PdfJsonExtractor.ts b/server/src/extractors/pdf2json/PdfJsonExtractor.ts new file mode 100644 index 00000000..eab8dbf9 --- /dev/null +++ b/server/src/extractors/pdf2json/PdfJsonExtractor.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document } from '../../types/DocumentRepresentation'; +import { extractFonts } from '../extract-fonts'; +import { Extractor } from '../Extractor'; +import * as pdf2json from './pdf2json'; + +/** + * The extractor is responsible to extract every possible information + * from the PDF File and store it in the Json file. + * It also ensure that the Json file is correctly formated and contains all the needed + * information in a clever way. + */ +export class PdfJsonExtractor extends Extractor { + public run(inputFile: string): Promise { + const pdf2jsonExtract: Promise = pdf2json.execute(inputFile); + + const extractFont = extractFonts(inputFile); + + return Promise.all([pdf2jsonExtract, extractFont]).then(([doc]: [Document, void]) => doc); + } +} diff --git a/server/src/extractors/pdf2json/pdf2json.ts b/server/src/extractors/pdf2json/pdf2json.ts new file mode 100644 index 00000000..80c66051 --- /dev/null +++ b/server/src/extractors/pdf2json/pdf2json.ts @@ -0,0 +1,164 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawn, spawnSync } from 'child_process'; +import * as fs from 'fs'; +import { XmlEntities } from 'html-entities'; +import * as os from 'os'; +import { BoundingBox, Document, Font, Page, Text, Word } from '../../types/DocumentRepresentation'; +import { Pdf2JsonFont } from '../../types/Pdf2JsonFont'; +import { Pdf2JsonPage } from '../../types/Pdf2JsonPage'; +import * as utils from '../../utils'; +import logger from '../../utils/Logger'; + +/** + * Executes the pdf2json extraction function, reading an input pdf file and extracting a document representation. + * This function involves recovering page contents like words, bounding boxes, fonts and other information that + * the pdf2json tool's output provides. This function spawns the externally existing pdf2json tool. + * + * @param pdfInputFile The path including the name of the pdf file for input. + * @returns The promise of a valid document (in the format DocumentRepresentation). + */ +export function execute(pdfInputFile: string): Promise { + const xmlEntities = new XmlEntities(); + return new Promise((resolve, reject) => { + return repairPdf(pdfInputFile).then(repairedPdf => { + const jsonOutputFile: string = utils.getTemporaryFile('.json'); + logger.debug(`pdf2json ${['-enc', 'UTF-8', repairedPdf, jsonOutputFile].join(' ')}`); + + if (!fs.existsSync(jsonOutputFile)) { + fs.appendFileSync(jsonOutputFile, ''); + } + + const pdf2json = spawn('pdf2json', ['-enc', 'UTF-8', repairedPdf, jsonOutputFile]); + + pdf2json.stderr.on('data', data => { + logger.error('pdf2json error:', data.toString('utf8')); + }); + + pdf2json.on('close', code => { + if (code === 0) { + logger.info('Reading json file...'); + const json: JSON = JSON.parse(fs.readFileSync(jsonOutputFile, 'utf8')); + const jsonPages = (json as any) as Pdf2JsonPage[]; + const pdfPages: Pdf2JsonPage[] = jsonPages.map(p => new Pdf2JsonPage(p)); + + const RATIO = 2 / 3; + const pdfFonts: Pdf2JsonFont[] = pdfPages + .map(pdfPage => pdfPage.fonts) + .reduce((a, b) => a.concat(b)); + const fonts: Font[] = []; + const pages: Page[] = pdfPages.map((pdfPage: Pdf2JsonPage) => { + const texts: Text[] = pdfPage.text + .map(pdfText => pdfTextToWord(pdfText, pdfFonts, fonts, RATIO, xmlEntities)) + .filter(word => { + // This can append sometimes with pdf2json + return ( + (word.box.width >= 0 && word.box.height >= 0) || word.toString().trim() !== '' + ); + }) + .map(word => { + word.box.width = Math.max(word.box.width, 0); + word.box.height = Math.max(word.box.height, 0); + return word; + }); + + return new Page( + pdfPage.number, + texts, + new BoundingBox(0, 0, pdfPage.width * RATIO, pdfPage.height * RATIO), + ); + }); + + const doc: Document = new Document(pages); + logger.debug('Done'); + resolve(doc); + } else { + reject(`pdf2json return code is ${code}`); + } + }); + }); + }); +} + +/** + * Converts a string of text into a valid word entity (as in the Document Representation format). + * + * @param pdfText The string to be converted into a word. + * @param pdfFonts List of fonts generated by the pdf2json tool. + * @param fonts A collection of fonts existing in the current document. + * @param RATIO The scaling ratio for the output word's bounding box. + * @param xmlEntities An xmlEntities object for decoding the pdfText data to generate the word content. + * @returns A valid Document Representation's word entity. + */ +function pdfTextToWord(pdfText, pdfFonts, fonts, RATIO, xmlEntities): Word { + const pdfFont: Pdf2JsonFont = pdfFonts.filter(f => f.fontspec === String(pdfText.font))[0]; + + const newFont = new Font(pdfFont.family, pdfFont.size, { color: pdfFont.color }); + const wordFont = findOrCreate(newFont, fonts); + + const word = new Word( + new BoundingBox( + pdfText.left * RATIO, + pdfText.top * RATIO, + pdfText.width * RATIO, + pdfText.height * RATIO, + ), + xmlEntities.decode(pdfText.data), + wordFont, + ); + + return word; +} + +/** + * Finds or creates a new font object + * @param newFont The name of the font to be searched or created + * @param fonts The list of existing fonts + * @returns A font object either containing an existing one that matches newFont, or a new object altogether. + */ +function findOrCreate(newFont: Font, fonts: Font[]): Font { + for (const font of fonts) { + if (font.isEqual(newFont)) { + return font; + } + } + + fonts.push(newFont); + return newFont; +} + +/** + * Repair a pdf using the external mutool utility. + * @param filePath The absolute filename and path of the pdf file to be repaired. + */ +function repairPdf(filePath: string) { + return new Promise(resolve => { + const mutoolPath = spawnSync('which', ['mutool']).output.join(''); + if (mutoolPath === '' || (/^win/i.test(os.platform()) && /no mutool in/.test(mutoolPath))) { + logger.warn('MuPDF not installed !! Skip clean PDF.'); + resolve(filePath); + } else { + const pdfOutputFile = utils.getTemporaryFile('.pdf'); + const pdfFixer = spawn('mutool', ['clean', filePath, pdfOutputFile]); + pdfFixer.on('close', () => { + // Check that the file is correctly written on the file system + fs.fsyncSync(fs.openSync(filePath, 'r+')); + resolve(pdfOutputFile); + }); + } + }); +} diff --git a/server/src/extractors/set-page-dimensions.ts b/server/src/extractors/set-page-dimensions.ts new file mode 100644 index 00000000..8747971c --- /dev/null +++ b/server/src/extractors/set-page-dimensions.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import { Document } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; + +interface Dimensions { + width: number; + height: number; +} + +export function setPageDimensions(doc: Document, inputFileName: string): Promise { + logger.debug('Setting page dimensions...'); + + return new Promise(resolve => { + getPageDimensions(inputFileName).then(dimensions => { + doc.pages.forEach((page, i) => { + page.width = dimensions[i].width; + page.height = dimensions[i].height; + // Remove weird false positive coming from Tesseract + page.elements = page.elements.filter(t => { + return t.height !== page.height || t.width !== page.width; + }); + }); + + resolve(doc); + }); + }); + + function getPageDimensions(filename: string): Promise { + return new Promise((resolve, reject) => { + const ret = spawnSync('identify', ['-format', '%[fx:w]x%[fx:h],', filename]); + + if (ret.status !== 0) { + logger.error(ret.stderr); + reject(`Can't get dimensions of file ${filename}`); + } + + const dimensions = ret.stdout.toString().split(','); + const retDimension: Dimensions[] = dimensions.map(dimension => { + const [width, height] = dimension.split('x').map(s => parseInt(s, 10)); + return { width, height }; + }); + + resolve(retDimension); + }); + } +} diff --git a/server/src/extractors/tesseract/TesseractExtractor.ts b/server/src/extractors/tesseract/TesseractExtractor.ts new file mode 100644 index 00000000..4960f26f --- /dev/null +++ b/server/src/extractors/tesseract/TesseractExtractor.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document } from '../../types/DocumentRepresentation'; +import { Extractor } from '../Extractor'; +import { setPageDimensions } from '../set-page-dimensions'; +import * as tesseract2json from './tesseract2json'; + +/** + * An extractor class to extract content from images using the tesseract OCR extraction tool. + */ +export class TesseractExtractor extends Extractor { + /** + * Runs the extraction process, first setting page dimentions, then extracting the document itself. + * @param inputFile The name of the image to be used at input for the extraction. + * @returns The promise of a valid Document (as per the Document Representation namespace). + */ + public run(inputFile: string): Promise { + return tesseract2json + .execute(inputFile, this.config) + .then((doc: Document) => setPageDimensions(doc, inputFile)); + } +} diff --git a/server/src/extractors/tesseract/tesseract2json.ts b/server/src/extractors/tesseract/tesseract2json.ts new file mode 100644 index 00000000..951e62ef --- /dev/null +++ b/server/src/extractors/tesseract/tesseract2json.ts @@ -0,0 +1,144 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawn, spawnSync } from 'child_process'; +import * as fs from 'fs'; +import { TSV } from 'tsv'; +import { Config } from '../../types/Config'; +import { BoundingBox, Document, Font, Page, Word } from '../../types/DocumentRepresentation'; +import { TsvElement } from '../../types/TsvElement'; +import * as utils from '../../utils'; +import logger from '../../utils/Logger'; + +/** + * Executes the tesseract to json conversion module, which entails calling + * @param imageInputFile The input image file to be executed using the tesseract OCR tool. + * @param config The input configuraiton for tesseract. + * @returns The promise of a valid Document (as in the Document Representation data structure). + */ +export function execute(imageInputFile: string, config: Config): Promise { + return new Promise((resolve, reject) => { + const tsvOutputFile: string = utils.getTemporaryFile('.json'); + + let configLanguages: string[]; + + if (typeof config.extractor.language === 'string') { + configLanguages = [config.extractor.language]; + } else if (Array.isArray(config.extractor.language)) { + configLanguages = config.extractor.language; + } else { + configLanguages = []; + } + + const langChecker = spawnSync('tesseract', ['--list-langs'], { + cwd: process.cwd(), + encoding: 'utf-8', + env: process.env, + stdio: 'pipe', + }); + + if (langChecker.error || !Array.isArray(langChecker.output)) { + throw new Error( + `tesseract --list-langs failed. Is tesseract correctly installed?.\n${langChecker.error}`, + ); + } + + const langs: string[][] = langChecker.output + .filter(value => value !== null) + .map(value => value.split(/\r?\n/)); + const langsFlat: string[] = [].concat.apply([], langs); + + const validLanguages: string[] = configLanguages.filter(lang => langsFlat.includes(lang)); + + if (validLanguages.length === 0) { + logger.info( + `the configuration is set to ${configLanguages}, but none of them are available on the system.`, + ); + logger.info('Defaulting to english (eng)'); + validLanguages.push('eng'); + } + + const tesseractLanguages = validLanguages.map(lang => lang.trim()).join('+'); + + /** + * From man page + * @param l The language to use. If none is specified, English is assumed. + * Multiple languages may be specified, separated by plus characters. + * Tesseract uses 3-character ISO 639-2 language codes. + */ + const tesseract = spawn('tesseract', [ + '-l', + tesseractLanguages, + imageInputFile, + tsvOutputFile, + 'tsv', + ]); + logger.debug( + `tesseract ${['-l', tesseractLanguages, imageInputFile, tsvOutputFile, 'tsv'].join(' ')}`, + ); + + tesseract.stdout.on('data', data => { + logger.debug('tesseract:', data.toString()); + }); + + // Tesseract spits out status information on stderr + tesseract.stderr.on('data', data => { + logger.debug('tesseract:', data.toString().trim()); + }); + + tesseract.on('close', code => { + if (code === 0) { + logger.info('Reading tsv file...'); + + const tsvContent: string = fs.readFileSync(tsvOutputFile + '.tsv', 'utf-8'); + const tsvOut: TsvElement[] = TSV.parse(tsvContent); + const pages: Page[] = []; + + tsvOut.forEach((elem: TsvElement) => { + if (typeof elem.text === 'undefined' || elem.text === '') { + return; + } + + const word: Word = new Word( + new BoundingBox(elem.left, elem.top, elem.width, elem.height), + String(elem.text), + new Font('Arial', 12), // TODO Proper font size + ); + + word.confidence = elem.conf; + + while (pages.length < elem.page_num) { + const page: Page = new Page( + elem.page_num, + [], + new BoundingBox(0, 0, 10000, 10000), // This is set by the setPageDimension module + ); + pages.push(page); + } + + pages[elem.page_num - 1].elements.push(word); + }); + + logger.info('Assigning object...'); + const doc: Document = new Document(pages); + logger.debug('Done'); + resolve(doc); + } else { + reject(`tesseract return code is ${code}`); + } + }); + }); +} diff --git a/server/src/modules/HeaderFooterDetectionModule.ts b/server/src/modules/HeaderFooterDetectionModule.ts new file mode 100644 index 00000000..78076ff4 --- /dev/null +++ b/server/src/modules/HeaderFooterDetectionModule.ts @@ -0,0 +1,205 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox, Document, Element, Paragraph, Text } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; +import { LinesToParagraphModule } from './LinesToParagraphModule'; +import { Module } from './Module'; + +/** + * Stability: Experimental + * Characterize marginals (header and footer) in a document. The word marginals comes + * from https://english.stackexchange.com/a/25105 + */ +interface Options { + ignorePages?: number[]; + maxMarginPercentage?: number; +} + +const defaultOptions: Options = { + ignorePages: [], + maxMarginPercentage: 30, +}; + +export class HeaderFooterDetectionModule extends Module { + public static moduleName = 'header-footer-detection'; + public static dependencies = [LinesToParagraphModule]; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + const opt: Options = { + maxMarginPercentage: 30, + ignorePages: [], + }; + Object.assign(opt, defaultOptions, this.options); + + const alreadyExist: boolean = + doc.pages + .map(p => { + return p.elements.filter(e => e.properties.isHeader || e.properties.isFooter).length; + }) + .reduce((a, b) => a + b, 0) > 0; + + if (doc.pages.length === 1) { + logger.warn( + 'Not computing marginals (headers and footers)' + + 'the document only has 1 page (not enough data).', + ); + return doc; + } else if (this.options.maxMarginPercentage === undefined) { + logger.info( + 'Not computing marginals (headers and footers); maxMarginPercentage setting not found in the configuration.', + ); + return doc; + } else if (alreadyExist) { + logger.warn( + 'Not computing marginals (headers and footers): header and footer data already exists.', + ); + return doc; + } + logger.info( + 'Detecting marginals (headers and footers) with maxMarginPercentage:', + this.options.maxMarginPercentage, + '...', + ); + + let occupancyAcrossHeight: number[] = []; + let occupancyAcrossWidth: number[] = []; + + function boolToInt(p) { + if (p) { + return 1; + } else { + return 0; + } + } + + doc.pages + .filter(p => !this.options.ignorePages.includes(p)) + .forEach(page => { + const h: number[] = page.horizontalOccupancy.map(boolToInt); + occupancyAcrossHeight = utils.addVectors(occupancyAcrossHeight, h); + const v: number[] = page.verticalOccupancy.map(boolToInt); + occupancyAcrossWidth = utils.addVectors(occupancyAcrossWidth, v); + }); + + // UNCOMMENT THESE TO EXPORT OCCUPANCIES INTO EXTERNAL CSV FILES + // writeFileSync("horizontal.csv", occupancyAcrossWidth.join(";"), {encoding: 'utf-8'}) + // writeFileSync("vertical.csv", occupancyAcrossHeight.join(";"), {encoding: 'utf-8'}) + + const heightZeros: number[] = utils + .findPositionsInArray(occupancyAcrossHeight, 0) + .sort((a, b) => { + return a - b; + }); + const widthZeros: number[] = utils + .findPositionsInArray(occupancyAcrossWidth, 0) + .sort((a, b) => { + return a - b; + }); + + const maxT: number = Math.floor( + 0 + (this.options.maxMarginPercentage * occupancyAcrossHeight.length) / 100, + ); + doc.margins.top = heightZeros + .filter(value => value < maxT) + .sort((a, b) => { + return b - a; + })[0]; + const maxB: number = Math.floor( + occupancyAcrossHeight.length - + (this.options.maxMarginPercentage * occupancyAcrossHeight.length) / 100, + ); + doc.margins.bottom = heightZeros + .filter(value => value > maxB) + .sort((a, b) => { + return a - b; + })[0]; + const maxL: number = Math.floor( + 0 + (this.options.maxMarginPercentage * occupancyAcrossWidth.length) / 100, + ); + doc.margins.left = widthZeros + .filter(value => value < maxL) + .sort((a, b) => { + return b - a; + })[0]; + const maxR: number = Math.floor( + occupancyAcrossWidth.length - + (this.options.maxMarginPercentage * occupancyAcrossWidth.length) / 100, + ); + doc.margins.right = widthZeros + .filter(value => value > maxR) + .sort((a, b) => { + return a - b; + })[0]; + + logger.info( + `Document margins for maxMarginPercentage ${this.options.maxMarginPercentage}: ` + + `top: ${doc.margins.top}, bottom: ${doc.margins.bottom}, ` + + `left: ${doc.margins.left}, right: ${doc.margins.right}`, + ); + + doc.pages.forEach(page => { + const headerElements: Element[] = page.getElementsSubset( + new BoundingBox(0, 0, page.width, doc.margins.top), + ); + + const footerElements: Element[] = page.getElementsSubset( + new BoundingBox(0, doc.margins.bottom, page.width, page.height - doc.margins.bottom), + ); + + for (const element of footerElements) { + element.properties.isFooter = true; + if (element instanceof Paragraph && isPageNumber(element)) { + element.properties.isPageNumber = true; + } + } + + for (const element of headerElements) { + element.properties.isHeader = true; + if (element instanceof Paragraph && isPageNumber(element)) { + element.properties.isPageNumber = true; + } + } + }); + logger.debug('Done with marginals detection.'); + return doc; + + /** + * Checks if a text is a page number using a regexp matching. + * @param text The text entity in question + */ + function isPageNumber(text: Text): boolean { + const match = text.toString().match(utils.getPageRegex()); + let pageNumber: string; + if (!match) { + return false; + } + + for (let i = 1; i < match.length; i++) { + if (match[i]) { + pageNumber = match[i]; + } + } + + return pageNumber && pageNumber !== ''; + } + } +} diff --git a/server/src/modules/HeadingDetectionModule.ts b/server/src/modules/HeadingDetectionModule.ts new file mode 100644 index 00000000..bb0d93c8 --- /dev/null +++ b/server/src/modules/HeadingDetectionModule.ts @@ -0,0 +1,153 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Font, Heading, Paragraph } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; +import { LinesToParagraphModule } from './LinesToParagraphModule'; +import { Module } from './Module'; + +// TODO This module doesn't work very well. It doesn't detect titles that are just bold text and +// should use the location of elements of the page to improve detection and accuary. +/** + * Stabiltiy: Experimental + * Detect title text blocks in the document, based on the font difference. + */ + +export class HeadingDetectionModule extends Module { + public static moduleName = 'heading-detection'; + public static dependencies = [LinesToParagraphModule]; + + public main(doc: Document): Document { + const existingHeaders = doc.pages + .map(page => { + return page.getElementsOfType(Heading).filter(t => t.toString().trim() !== ''); + }) + .reduce((a, b) => a.concat(b), []); + + if (existingHeaders.length !== 0) { + logger.warn('Not detecting titles: the document already contains them.'); + return doc; + } + + const paragraphs: Paragraph[] = doc.pages + .map(page => { + return page.getElementsOfType(Paragraph).filter(t => t.toString().trim() !== ''); + }) + .reduce((a, b) => a.concat(b), []); + + const sizeProportion: Map = new Map(); + const nameProportion: Map = new Map(); + const italicProportion: Map = new Map(); + const underlineProportion: Map = new Map(); + const colorProportion: Map = new Map(); + const weightProportion: Map = new Map(); + + paragraphs.forEach((paragraph: Paragraph) => { + const font: Font = paragraph.getMainFont(); + sizeProportion.set(font.size, Math.trunc(sizeProportion.get(font.size)) + 1); + nameProportion.set(font.name, Math.trunc(nameProportion.get(font.name)) + 1); + italicProportion.set(font.isItalic, Math.trunc(italicProportion.get(font.isItalic)) + 1); + underlineProportion.set( + font.isUnderline, + Math.trunc(underlineProportion.get(font.isUnderline)) + 1, + ); + colorProportion.set(font.color, Math.trunc(colorProportion.get(font.color)) + 1); + weightProportion.set(font.weight, Math.trunc(weightProportion.get(font.weight)) + 1); + }); + + const mostCommonSize: [number, number] = Array.from(sizeProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + [0, 0], + ); + const mostCommonWeight: [string, number] = Array.from(weightProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + ['', 0], + ); + const mostCommonItalic: [boolean, number] = Array.from(italicProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + [false, 0], + ); + const mostCommonUnderline: [boolean, number] = Array.from(underlineProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + [false, 0], + ); + const mostCommonColor: [string, number] = Array.from(colorProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + ['', 0], + ); + const mostCommonName: [string, number] = Array.from(nameProportion).reduce( + (a, b) => (a[1] > b[1] ? a : b), + ['', 0], + ); + + const allLevels: Set = new Set([]); + paragraphs.forEach((paragraph: Paragraph) => { + const scores = { + size: 0, + weight: 0, + color: 0, + name: 0, + italic: 0, + underline: 0, + }; + + scores.size = paragraph.getMainFont().size / mostCommonSize[0]; + scores.weight = paragraph.getMainFont().weight !== mostCommonWeight[0] ? 1 : 0; + scores.italic = paragraph.getMainFont().isItalic !== mostCommonItalic[0] ? 1 : 0; + scores.underline = paragraph.getMainFont().isUnderline !== mostCommonUnderline[0] ? 1 : 0; + scores.color = paragraph.getMainFont().color !== mostCommonColor[0] ? 1 : 0; + scores.name = paragraph.getMainFont().name !== mostCommonName[0] ? 1 : 0; + + paragraph.properties.titleScores = scores; + allLevels.add(paragraph.getMainFont().size); + }); + + titleFromSize(1.3); + + /* + let titleNb = 0; + if (titleNb < texts.length * 0.1) { + texts.forEach(t => { + + }); + } + */ + + return doc; + + function titleFromSize(threshold: number) { + const levels: number[] = Array.from(allLevels).sort((a, b) => b - a); + paragraphs.forEach((paragraph: Paragraph) => { + const scores = paragraph.properties.titleScores; + + if (scores.size > threshold) { + const heading: Heading = new Heading(paragraph.box, paragraph.content); + heading.language = paragraph.language; + heading.level = levels.indexOf(heading.getMainFont().size) + 1; + heading.metadata = paragraph.metadata; + heading.properties = paragraph.properties; + heading.parent = paragraph.parent; + heading.redundant = paragraph.redundant; + + doc = utils.replaceObject(doc, paragraph, heading); + + // titleNb++; + } + }); + } + } +} diff --git a/server/src/modules/HierarchyDetectionModule.ts b/server/src/modules/HierarchyDetectionModule.ts new file mode 100644 index 00000000..fd27076f --- /dev/null +++ b/server/src/modules/HierarchyDetectionModule.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Element, Heading, List, Paragraph } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import { Module } from './Module'; + +type Parent = Heading | Paragraph | List; + +/** + * + * Parents may be: + * - Headings + * - Paragraphs before List + * - List before a child List + * @param doc + */ + +export class HierarchyDetectionModule extends Module { + public static moduleName = 'hierarchy-detection'; + + public main(doc: Document): Document { + const parents: Parent[] = []; + let previousElement: Parent = null; + let currentHeadingLevel: number = -1; + + doc.pages.forEach(page => { + // TODO Remove when Cleaner modules have proper dependencies + page.elements.sort(utils.sortElementsByOrder); + page.elements.forEach((element: Element) => { + if (element instanceof Heading) { + if (element.level > currentHeadingLevel) { + associate(element, getLastHeading()); + } else { + removeParentsToHeading(element.level); + } + + parents.push(element); + currentHeadingLevel = element.level; + previousElement = element; + } else if (element instanceof List) { + if (previousElement instanceof List) { + if (element.level > previousElement.level) { + parents.push(previousElement); + associate(element, previousElement); + } else { + removeListsFromParents(element.level); + associate(element, getLastParent()); + } + } else if (previousElement instanceof Heading) { + associate(element, previousElement); + } else if (previousElement instanceof Paragraph) { + parents.push(previousElement); + associate(element, previousElement); + } + + previousElement = element; + } else if (element instanceof Paragraph) { + associate(element, getLastParent()); + previousElement = element; + } else { + associate(element, getLastParent()); + } + + if (!(element instanceof List)) { + removeParentsToHeading(); + } + }); + }); + + return doc; + + function getLastParent(): Parent | void { + return parents.length > 0 ? parents[parents.length - 1] : null; + } + + function getLastHeading(): Heading | void { + for (const parent of parents) { + if (parent instanceof Heading) { + return parent; + } else { + return null; + } + } + } + + function associate(child: Element | void, parent: Parent | void) { + if (child && parent) { + child.parent = parent; + parent.children.push(child); + } + } + + /** + * Remove parents one by one until the level is strictly lower then level. + * @param headingLevel + */ + function removeParentsToHeading(headingLevel: number = Infinity): void { + let i = parents.length - 1; + + while (i >= 0) { + const parent: Parent = parents[i]; + + if (!(parent instanceof Heading) || parent.level >= headingLevel) { + parents.pop(); + } else { + return; + } + + i--; + } + } + + function removeListsFromParents(listLevel: number = -1): void { + let i = parents.length - 1; + + while (i >= 0) { + const parent: Parent = parents[i]; + + if (!(parent instanceof List) || parent.level < listLevel) { + return; + } else { + parents.pop(); + } + + i--; + } + } + } +} diff --git a/server/src/modules/KeyValueDetectionModule.ts b/server/src/modules/KeyValueDetectionModule.ts new file mode 100644 index 00000000..b4959d19 --- /dev/null +++ b/server/src/modules/KeyValueDetectionModule.ts @@ -0,0 +1,238 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as string_similarity from 'string-similarity'; +import { BoundingBox, Document, Line, Page, Word } from '../types/DocumentRepresentation'; +import { KeyValueMetadata } from '../types/Metadata/KeyValueMetadata'; +import { getSubCollections } from '../utils'; +import logger from '../utils/Logger'; +import { Module } from './Module'; +import { WordsToLineModule } from './WordsToLineModule'; + +interface Options { + keyPatterns?: object; + threshold?: number; +} + +const defaultOptions: Options = { + keyPatterns: {}, + threshold: 0.2, +}; + +export type KeyCandidate = { + words: Word[]; + matchingPattern: string; + score: number; + keyName: string; +}; + +/** + * Detect key value pairs in a document, of the type , using a threshold probability. + */ +export class KeyValueDetectionModule extends Module { + public static moduleName = 'key-value-detection'; + public static dependencies = [WordsToLineModule]; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + const opt: Options = { + keyPatterns: {}, + threshold: 0.2, + }; + Object.assign(opt, defaultOptions, this.options); + + if (this.options.threshold === undefined) { + logger.info('Not computing key-value pairs, no threshold vas specified'); + return doc; + } else if (this.options.keyPatterns === {}) { + logger.info('The key patterns not precised. Not computing key-value pairs.'); + return doc; + } else { + logger.info( + 'Detecting key-value pairs, for key patterns', + Object.keys(this.options.keyPatterns), + '...', + ); + } + + // Extract collections of key-matches for each permutation of word length in a particular line, + // then select the highest scoring matches above the limiting threshold. + doc.pages.forEach(page => { + logger.debug('----------------- page', page.pageNumber); + const allLines: Line[] = page.getElementsOfType(Line); + const allKeys: KeyCandidate[] = []; + + for (const [key, patterns] of Object.entries(this.options.keyPatterns)) { + allLines.forEach(line => { + if (Array.isArray(patterns)) { + const bestKey: KeyCandidate = patterns + .map(p => { + return this.findKeys(key, p, line.content).sort((a, b) => b.score - a.score)[0]; + }) + .filter(k => typeof k !== 'undefined' && k.words.length !== 0) + .filter(c => c.score > this.options.threshold) + .reduce(this.takeBestScore, { + score: 0, + words: [], + matchingPattern: '', + keyName: '', + }); + if (bestKey.score !== 0) { + allKeys.push(bestKey); + } + } + }); + } + + const uniqueKeys: KeyCandidate[] = []; + const overlappingKeysArray: KeyCandidate[][] = []; + + // Using bins to group keys by word they apply to + allKeys.forEach(key1 => { + // Try to find a bin with overlapping words + const overlappingKeys = overlappingKeysArray.filter(o => { + return o.some(key2 => key1.words.some(w => key2.words.includes(w))); + }); + + if (overlappingKeys.length > 0) { + overlappingKeys[0].push(key1); + } else { + overlappingKeysArray.push([key1]); + } + }); + + overlappingKeysArray.forEach(overlappingKeys => { + const highestScoreKey: KeyCandidate = overlappingKeys.reduce(this.takeBestScore, { + score: 0, + words: [], + matchingPattern: '', + keyName: '', + }); + + uniqueKeys.push(highestScoreKey); + }); + + uniqueKeys.forEach(key => { + const values: Word[] = this.findValues(key, uniqueKeys, page); + + if (values.length > 0) { + const metadata: KeyValueMetadata = new KeyValueMetadata([...key.words, ...values], { + keyName: key.keyName, + keyElements: key.words, + valueElements: values, + }); + + logger.debug( + `${key.keyName}: ${key.words.map(k => k.toString())}, ${values.map(k => + k.toString(), + )} - ${key.score}`, + ); + + metadata.elements.forEach(v => v.metadata.push(metadata)); + } + }); + }); + + logger.debug('Done with key-value pair detection'); + return doc; + } + + /** + * + * @param entry The KeyCandidate with which the keys are to be matched. + * @param keyCandidates All the key candidates, so that a value inside these key candidates is not matched. + * @param page The current page on which the key's values are to be matched. + */ + private findValues(entry: KeyCandidate, keyCandidates: KeyCandidate[], page: Page): Word[] { + const keyBox: BoundingBox = BoundingBox.merge(entry.words.map(w => w.box)); + + const nextKeyBox: BoundingBox = keyCandidates + .map(keyCandidate => { + return BoundingBox.merge(keyCandidate.words.map(w => w.box)); + }) + .filter((box: BoundingBox) => { + return box.bottom >= keyBox.top && box.top <= keyBox.bottom && box.left >= keyBox.right; + }) + .reduce((a, b) => { + if (a.left <= b.left) { + return a; + } else { + return b; + } + }, new BoundingBox(Infinity, Infinity, Infinity, Infinity)); + + return page.getElementsOfType(Word).filter(word => { + return ( + word.bottom >= keyBox.top && + word.top <= keyBox.bottom && + word.left >= keyBox.right && + word.right <= nextKeyBox.left && + keyCandidates.every(keyCandindate => !keyCandindate.words.includes(word)) && + !this.options.keyValueDividerChars.includes(word.toString()) + ); + }); + } + + /** + * + * @param key A key to be matched, corresponding to the name of a value class to be matched. + * @param pattern the string pattern to be matched with each candidate. + * @param words All the words for the pattern to be matched with. + */ + private findKeys(key, pattern: string, words: Word[]): KeyCandidate[] { + const filteredWords: Word[] = words.filter( + w => !this.options.keyValueDividerChars.includes(w.toString()), + ); + + let wordCollections: Word[][] = []; + for (let len = 1; len !== words.length; ++len) { + wordCollections = [...wordCollections, ...getSubCollections(filteredWords, len)]; + } + + const bestMatch: KeyCandidate = { + score: 0.0, + words: [], + matchingPattern: pattern, + keyName: key, + }; + + const keys: KeyCandidate[] = wordCollections.map(wc => { + const wcString: string = wc + .map(w => w.toString().trim()) + .reduce((w1, w2) => w1 + ' ' + w2, '') + .trim() + .replace(new RegExp(`(?:\\${this.options.keyValueDividerChars.join('|\\')})`), ''); + const score: number = string_similarity.compareTwoStrings(wcString, pattern); + if (score > bestMatch.score) { + bestMatch.words = wc; + bestMatch.score = score; + } + return bestMatch; + }); + return keys; + } + + private takeBestScore(a: KeyCandidate, b: KeyCandidate) { + if (a.score >= b.score) { + return a; + } else { + return b; + } + } +} diff --git a/server/src/modules/LinesToParagraphModule.ts b/server/src/modules/LinesToParagraphModule.ts new file mode 100644 index 00000000..0b631c02 --- /dev/null +++ b/server/src/modules/LinesToParagraphModule.ts @@ -0,0 +1,134 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BoundingBox, + Document, + Element, + Heading, + Line, + Page, + Paragraph, +} from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; +import { Module } from './Module'; +import { ReadingOrderDetectionModule } from './ReadingOrderDetectionModule'; +import { WordsToLineModule } from './WordsToLineModule'; + +interface Options { + addNewline?: boolean; + alignUncertainty?: number; // value in px + checkFont?: boolean; + lineLengthUncertainty?: number; // factor of line width + maxInterline?: number; // factor of line height +} + +const defaultOptions = { + addNewline: true, + alignUncertainty: 3, + checkFont: false, + maxInterline: 0.3, + lineLengthUncertainty: 0.25, +}; + +/** + * Stability: Stable + * Merge lines into paragraphs + */ +export class LinesToParagraphModule extends Module { + public static moduleName = 'lines-to-paragraph'; + public static dependencies = [ReadingOrderDetectionModule, WordsToLineModule]; + + constructor(options: Options = {}) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + doc.pages.forEach((page: Page) => { + if (page.getElementsOfType(Paragraph).length > 0) { + logger.warn( + 'Warning: this page already has some paragraphs in it. Not performing paragraph merge.', + ); + return page; + } + + const lines: Line[] = page.getElementsOfType(Line).sort(utils.sortElementsByOrder); + const toBeMerged: Line[][] = []; + const otherElements: Element[] = page.elements.filter( + element => !(element instanceof Line) || !lines.includes(element), + ); + + for (let i = 0; i < lines.length; i++) { + const firstLine: Line = lines[i]; + const mergeGroup: Line[] = [firstLine]; + + for (let j = i + 1; j < lines.length; j++) { + const prev: Line = lines[j - 1]; + const curr: Line = lines[j]; + + if ( + //// FIXME (!this.options.checkFont || line1.font === line2.font) && + this.isAdjacentLine(prev, curr) && + (utils.isAligned([prev, curr], this.options.alignUncertainty) || + utils.isAlignedCenter([prev, curr], this.options.alignUncertainty)) && + // isntBulletList(prev, curr) && + // TODO handle table elements: !line1.properties.isTableElement && + // TODO handle table elements: !line2.properties.isTableElement && + prev instanceof Heading === curr instanceof Heading + ) { + mergeGroup.push(curr); + i++; + } else { + // i = j; + break; + } + } + + toBeMerged.push(mergeGroup); + } + + let newOrder = 0; + const paragraphs: Paragraph[] = toBeMerged.map((group: Line[]) => { + const paragraph: Paragraph = utils.mergeElements( + new Paragraph(new BoundingBox(0, 0, 0, 0)), + ...group, + ); + paragraph.properties.order = newOrder++; + return paragraph; + }); + + page.elements = otherElements.concat(paragraphs); + + return page; + }); + + return doc; + } + + /** + * Checks if two lines are adjacent or not by using a measure of their overlap uncertainty. + * @param line1 the first line + * @param line2 the second line + */ + private isAdjacentLine(line1: Line, line2: Line): boolean { + const verticalOverlapUncertainty = (line1.height * 2) / 3; + return ( + line1.top + line1.height < line2.top + verticalOverlapUncertainty && + line1.top + line1.height * (1 + this.options.maxInterline) > line2.top + ); + } +} diff --git a/server/src/modules/LinkDetectionModule.ts b/server/src/modules/LinkDetectionModule.ts new file mode 100644 index 00000000..f93c5891 --- /dev/null +++ b/server/src/modules/LinkDetectionModule.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Page, Word } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; +import { Module } from './Module'; + +// TODO Test on other types of links (actionGoToR, actionLaunch, etc.) +// Maybe HTML isn't the right format to use. +// Putting all of this as metadata/property may be a better solution. +/** + * Stability: Experimental + * Convert PDF links to HTML links + */ + +export class LinkDetectionModule extends Module { + public static moduleName = 'link-detection'; + + public main(doc: Document): Document { + // actionURI(http://www.axa.com):www.axa.com + const actionUriRegex = /(.*)actionURI\((.*)\):(.*)/; + // actionGoTo:7,In Real Life + const actionGoToRegex = /(.*)actionGoTo:(\d+),(.*)/; + // Ref: http://www.cs.cmu.edu/~lemur/doxygen/lemur-3.1/html/Link_8h.html + const actionRegex = /(.*)(actionGoToR|actionLaunch|actionNamed|actionMovie|actionUnknown)(.*)/; + + doc.pages.forEach((page: Page) => { + page.getElementsOfType(Word).forEach(word => { + let match = []; + if (typeof word.content !== 'string') { + return; + } + // tslint:disable-next-line:no-conditional-assignment + if ((match = word.content.match(actionUriRegex))) { + word.content = `${match[1]}${match[3]}`; + // tslint:disable-next-line:no-conditional-assignment + } else if ((match = word.content.match(actionGoToRegex))) { + word.content = match[1] + match[3]; + // tslint:disable-next-line:no-conditional-assignment + } else if ((match = word.content.match(actionRegex))) { + logger.debug('Unknown action: %s', word.content); + } + }); + }); + + return doc; + } +} diff --git a/server/src/modules/ListDetectionModule.ts b/server/src/modules/ListDetectionModule.ts new file mode 100644 index 00000000..31e0c950 --- /dev/null +++ b/server/src/modules/ListDetectionModule.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Text } from '../types/DocumentRepresentation'; +import { Module } from './Module'; + +// TODO Handle ordered list. +/** + * Stability: Unstable + * Merge lines containing bullet points characters and tag them accordingly. + * Doesn't handle odered list (with bullet such as `1)`, `I.`, `a)`, `i.`, etc.) yet. + */ +export class ListDetectionModule extends Module { + public static moduleName = 'list-detection-module'; + + public main(doc: Document): Document { + const maxSpace = 60; // space width between bullet and text in px + const maxBulletLength = 3; + + doc.pages.forEach(page => { + // let texts: Text[] = page.getTexts(); + }); + + return doc; + + function isAligned(bullet: Text, text: Text): boolean { + return ( + bullet.left + bullet.width + maxSpace >= text.left && + bullet.left < text.left + text.width && + ((bullet.top <= text.top && bullet.top + bullet.height >= text.top) || + (bullet.top >= text.top && bullet.top <= text.top + text.height)) + ); + } + } +} diff --git a/server/src/modules/Module.ts b/server/src/modules/Module.ts new file mode 100644 index 00000000..3d587cee --- /dev/null +++ b/server/src/modules/Module.ts @@ -0,0 +1,57 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; + +export class Module { + public static moduleName: string = ''; + public static dependencies: Array = []; + private _options: any = {}; + + constructor(options?: T, defaultOptions?: T) { + this._options = { ...defaultOptions, ...options }; + } + + public run(document: Document): Promise { + return Promise.resolve(this.main(document)); + } + + public bypass(document: Document): Promise { + return Promise.resolve(document); + } + + /** + * Getter options + * @return {any} + */ + public get options(): any { + return this._options; + } + + /** + * Setter options + * @param {any} value + */ + public set options(value: any) { + this._options = value; + } + + protected main(document: Document): Document | Promise { + logger.warn('Module main should not be called.'); + return this.bypass(document); + } +} diff --git a/server/src/modules/NumberCorrectionModule.ts b/server/src/modules/NumberCorrectionModule.ts new file mode 100644 index 00000000..c96841f0 --- /dev/null +++ b/server/src/modules/NumberCorrectionModule.ts @@ -0,0 +1,270 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + BoundingBox, + Character, + Document, + Line, + Page, + Word, +} from '../types/DocumentRepresentation'; +import { Module } from './Module'; + +/** + * Stability: Experimental + * Merge elements or replace contents to make some words look like numbers when + * they are matching a pattern. + */ + +interface Options { + numberRegExp?: RegExp; + fixSplitNumbers?: boolean; + maxConsecutiveSplits?: number; + whitelist?: Set; +} + +const defaultOptions: Options = { + // This regexp might be changed depending of the language and number notation. + // French accounting notation: 1234567,89 + // US English scientific notation: 1.892323 + // US English accounting notation: 1,234,567.89 + fixSplitNumbers: true, + maxConsecutiveSplits: 3, + numberRegExp: RegExp('^([0-9]{1,3},([0-9]{3},)*[0-9]{3}|[0-9]+)(\\.[0-9]{2})?$'), + whitelist: new Set(), +}; + +function charArrayToString(input: Character[] | string): string { + if (Array.isArray(input)) { + const a = input.map(char => char.content).join(''); + return a; + } + return input; +} + +export class NumberCorrectionModule extends Module { + public static moduleName = 'number-correction'; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + const options = Object.assign(defaultOptions, this.options); + doc.pages.forEach(page => { + // Correct numbers embedded into Words + this.correctWords(page); + // Correct numbers that might be split by mistake into exactly 2 Words under a Line node. + if (options.fixSplitNumbers) { + this.correctLines(page); + } + }); + + return doc; + } + + public suggestNumberCorrections( + inputStr: string, + numberRegExp: RegExp, + whitelist?: Set, + preCandidates?: Map, + ): Array<[string, number]> { + const proximityLetters = { + ',': '.', + D: '0', + I: '1', + O: '0', + S: '5', + T: '1', + o: '0', + }; + + const deletes = ' "\''.split(''); + + function isValidNumber(input: string): boolean { + return numberRegExp.test(input); + } + + function generateEdits(word: string) { + const results: string[] = []; + + // Proximity replacements + let proximity = word; + Object.keys(proximityLetters).forEach(letter => { + const re = RegExp(letter, 'g'); + proximity = proximity.replace(re, proximityLetters[letter]); + }); + results.push(proximity); + + // Right-most comma replacement 1,232,20 -> 1,232.20 + if (RegExp(',[0-9]{2}$').test(word)) { + results.push(word.slice(0, word.length - 3) + '.' + word.slice(word.length - 2)); + } + + // Insert decimal separator for accounting notation only with leading 0 + // 000 -> 0.00 + // 00001 -> 0.0001 + // 12345 -> 12345 + if (RegExp('^0[^\\.]+$').test(word)) { + results.push(word.slice(0, 1) + '.' + word.slice(1)); + } + + // Deletes + deletes.forEach(letterToDel => { + const re = RegExp(letterToDel, 'g'); + results.push(word.replace(re, '')); + }); + + return results; + } + + function checkAndPopulateCanditates(list: string[], score: number): Map { + const candidates = new Map(); + list.forEach(edit => { + if (isValidNumber(edit) /* && !candidates.has(edit) */) { + candidates.set(edit, score); + } + }); + return candidates; + } + + function suggest(input: string): Array<[string, number]> { + // bail out if the input is too large - TODO: move this to the options + if (input.length > 16) { + return new Array<[string, number]>(); + } + // bail out if the input is white listed + if (whitelist && whitelist.has(input)) { + return new Array<[string, number]>(); + } + + // Generate a scored set of candidates + let candidates: Map = preCandidates + ? preCandidates + : new Map(); + if (isValidNumber(input)) { + candidates.set(input, 0); + } + const editsLevel1 = generateEdits(input); + candidates = new Map([...checkAndPopulateCanditates(editsLevel1, 1), ...candidates]); + editsLevel1.forEach(edit1 => { + // Generate 2nd level edits + // We need at least 2 level edits to get from ooo => 000 (level 1) => 0.00 (level 2) + // We could consider going deeper with generators (to save a bit of memory). + const editsLevel2 = generateEdits(edit1); + // Scoring might be improved, we currently consider bigger changes as better changes. + candidates = new Map([...checkAndPopulateCanditates(editsLevel2, 2), ...candidates]); + }); + + const sortedCandidates = Array.from(candidates.keys()) + .sort((a: string, b: string) => { + return candidates.get(b) - candidates.get(a); + }) + .map( + (k: string): [string, number] => { + return [k, candidates.get(k)]; + }, + ); + // Return an ordered list, high scoring changes should be more important + // and preferred suggestions + return sortedCandidates; + } + + return suggest(inputStr); + } + + private correctWord(word: Word, preCandidates?: Map) { + const content = charArrayToString(word.content); + const suggestion = this.getBestSuggestion(content, preCandidates); + if (suggestion) { + word.content = suggestion; + } + } + + private getBestSuggestion(input: string, preCandidates?: Map): string | null { + const suggestions = this.suggestNumberCorrections( + input, + this.options.numberRegExp, + this.options.whitelist, + preCandidates, + ); + if (suggestions.length > 0) { + return suggestions[0][0]; + } + return null; + } + + private correctWords(page: Page) { + page.getElementsOfType(Word).forEach(word => { + this.correctWord(word); + }); + } + + private scorePreCandidate(input: string): number { + let score = 0; + if (RegExp('\\.[0-9]{2}$').test(input)) { + score += 10; + } + return score; + } + + private generateCandidateMap(candidates: Word[]): Map { + const scoredCandidates = new Map(); + candidates.forEach(word => { + const wordContent = charArrayToString(word.content); + scoredCandidates.set(wordContent, this.scorePreCandidate(wordContent)); + }); + return scoredCandidates; + } + + private correctLines(page: Page) { + page.getElementsOfType(Line).map(line => { + if ( + line.content && + line.content.length >= 2 && + line.content.length <= this.options.maxConsecutiveSplits + ) { + // Make sure words are numbers + const consecutiveWordsAreNumbers = line.content.reduce((acc, word) => { + return acc && /^[0-9,. "']+$/.test(charArrayToString(word.content)); + }, true); + if (!consecutiveWordsAreNumbers) { + return; + } + const candidates: Word[] = [' ', ',', '.'].map(sep => + line.content.reduce((acc, e) => { + return new Word( + BoundingBox.merge([e.box, acc.box]), + charArrayToString(acc.content) + sep + charArrayToString(e.content), + e.font, + e.language, + ); + }), + ); + const defaultCandidate = candidates[0]; + const suggestion = this.getBestSuggestion( + charArrayToString(defaultCandidate.content), + this.generateCandidateMap(candidates), + ); + if (suggestion) { + defaultCandidate.content = suggestion; + line.content = [defaultCandidate]; + } + } + }); + } +} diff --git a/server/src/modules/OutOfPageRemovalModule.ts b/server/src/modules/OutOfPageRemovalModule.ts new file mode 100644 index 00000000..24466d24 --- /dev/null +++ b/server/src/modules/OutOfPageRemovalModule.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Element, Page, Text } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import { Module } from './Module'; + +/** + * Stabiltiy: Stable + * Remove any elements that are strictly out of the page. + * Items that overlap with the side of the page will be kept. + */ +export class OutOfPageRemovalModule extends Module { + public static moduleName = 'out-of-page-removal'; + + public main(doc: Document): Document { + doc.pages.forEach((page: Page) => { + page.elements = page.elements.filter((element: Element) => { + return !(element instanceof Text) || utils.isInBox(element.box, page.box); + }); + }); + + return doc; + } +} diff --git a/server/src/modules/ReadingOrderDetectionModule.ts b/server/src/modules/ReadingOrderDetectionModule.ts new file mode 100644 index 00000000..e303d145 --- /dev/null +++ b/server/src/modules/ReadingOrderDetectionModule.ts @@ -0,0 +1,204 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Element, Page } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import { Module } from './Module'; + +// TODO Handle rtl (right-to-left) languages +/** + * Stability: Stable + * Detect the reading order of the document. + * Add a property order tag to every text block: `{ 'order': number }` + */ + +interface Options { + minWidth?: number; +} + +const defaultOptions: Options = { + minWidth: 5, +}; + +export class ReadingOrderDetectionModule extends Module { + public static moduleName = 'reading-order-detection'; + private order: number = 0; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + doc.pages = doc.pages.map((page: Page) => { + // FIXME Hotfix because this algo bugs with floating point number + const elements: Element[] = page.elements.filter(Element.hasBoundingBox); + + this.order = 0; + this.process(elements); + + elements.sort(utils.sortElementsByOrder); + page.elements = elements; + + return page; + }); + + return doc; + } + + private process(elements: Element[]): void { + const verticalGroups = this.findVerticalGroups(elements); + this.processVerticalGroups(verticalGroups); + } + + private processVerticalGroups(groups: Element[][]): void { + groups.forEach(group => { + const horizontalGroups = this.findHorizontalGroups(group); + const superHorizontalGroups = this.findHorizontalSuperGroups(horizontalGroups); + this.processHorizontalGroups(superHorizontalGroups); + }); + } + + // A "super group" is a set of horizontal groups with possible common vertical cuts + private findHorizontalSuperGroups(groups: Element[][]): Element[][] { + const superGroups: Element[][] = []; + + groups.forEach(group => { + if (superGroups.length === 0) { + superGroups.push(group); + } else { + const curSuperGroup: Element[] = superGroups[superGroups.length - 1]; + const commonVerticalGroups = this.findVerticalGroups([...curSuperGroup, ...group]); + if (commonVerticalGroups.length > 1) { + superGroups[superGroups.length - 1] = [...curSuperGroup, ...group]; + } else { + superGroups.push(group); + } + } + }); + + return superGroups; + } + + private processHorizontalGroups(groups: Element[][]): void { + if (groups.length > 1) { + groups.forEach(group => { + const verticalGroups = this.findVerticalGroups(group); + this.processVerticalGroups(verticalGroups); + }); + } else if (groups.length === 1) { + this.processBlock(groups[0]); + } + } + + private processBlock(group: Element[]): void { + group.sort((a, b) => { + // Some line are not really flat. This fixes the uncertainty. + if (Math.abs(a.top - b.top) > Math.min(a.height, b.height) / 2) { + return a.top - b.top; + } else { + return a.left - b.left; + } + }); + + group.forEach(element => { + element.properties.order = this.order++; + }); + } + + private findHorizontalGroups(elements: Element[]): Element[][] { + const elementsGroups: Element[][] = []; + let elementsRest: Element[] = elements; + + let bottommost: number = 0; + let startGroup: number = 0; + + while (elementsRest.filter(e => e.top >= bottommost).length > 0) { + elementsRest = elementsRest.filter(e => e.top >= bottommost); + const elementsTopSides: number[] = elementsRest.map(e => e.top); + + elementsRest.sort((a, b) => a.top - b.top); + const sortedTopElements: Element[] = elementsRest; + + startGroup = Math.min(...elementsTopSides.filter(top => top > bottommost)); + + let group: Element[] = [sortedTopElements[0]]; + bottommost = sortedTopElements[0].bottom; + let previousBottommost: number; + + // Eat every included elements before a blank + do { + previousBottommost = bottommost; + + elementsRest.forEach(e => { + if (e.top <= bottommost && e.top >= startGroup && !group.includes(e)) { + group.push(e); + } + }); + + bottommost = Math.max(...group.map(e => e.bottom)); + } while (previousBottommost !== bottommost); + + elementsGroups.push(group); + group = []; + } + + return elementsGroups; + } + + private findVerticalGroups(elements: Element[]): Element[][] { + const elementsGroups: Element[][] = []; + let elementsRest: Element[] = elements; + + let rightmost: number = 0; + let startGroup: number = 0; + + while (elementsRest.filter(e => e.left > rightmost).length > 0) { + elementsRest = elementsRest.filter(e => e.left > rightmost); + const elementsLeftSides: number[] = elementsRest.map(e => e.left); + + elementsRest.sort((a, b) => a.left - b.left); + const sortedLeftElements: Element[] = elementsRest; + + startGroup = Math.min(...elementsLeftSides.filter(left => left > rightmost)); + + let group: Element[] = [sortedLeftElements[0]]; + rightmost = sortedLeftElements[0].right; + let previousRightmost: number; + + // Eat every included elements before a blank + do { + previousRightmost = rightmost; + + elementsRest.forEach(e => { + if ( + e.left <= rightmost + this.options.minWidth && + e.left >= startGroup && + !group.includes(e) + ) { + group.push(e); + } + }); + + rightmost = Math.max(...group.map(e => e.right)); + } while (previousRightmost !== rightmost); + + elementsGroups.push(group); + group = []; + } + + return elementsGroups; + } +} diff --git a/server/src/modules/RedundancyDetectionModule.ts b/server/src/modules/RedundancyDetectionModule.ts new file mode 100644 index 00000000..ad90fb3d --- /dev/null +++ b/server/src/modules/RedundancyDetectionModule.ts @@ -0,0 +1,120 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Page, Text } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import { Module } from './Module'; + +interface Options { + percentageOfRedondancy?: number; + minimumPages?: number; +} + +const defaultOptions: Options = { + percentageOfRedondancy: 0.5, + minimumPages: 6, +}; + +// TODO Idea split large document every 100 pages or so. +/** + * Stability: Unstable + * Detect items that are redundant on a certain amount of pages (i.e. 20% of every pages has the same element). + * Also remove duplicated elements. + */ + +export class RedundancyDetectionModule extends Module { + public static moduleName = 'redundancy-detection'; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + // Blocks that have the same bounding boxes on a lot of pages + // With a very similar content + // With the same font + + // let texts: Text[] = doc.pages.map(page => page.getTexts()).reduce((a, b) => a.concat(b), []); + doc.pages.forEach(page => { + const groups: Text[][] = regroupTextsByLocation(page.getTexts()); + removeDuplicateElements(page, groups); + // let redundants: Text[][] = tagRedundant(groups); + }); + + return doc; + + // FIXME this function is super slow... (36s on t6.pdf) + function regroupTextsByLocation(texts: Text[]): Text[][] { + const groups: Text[][] = []; + + texts.forEach(text => { + for (const group of groups) { + if (utils.isAlignedAndOverlapVertically(group.concat(text))) { + group.push(text); + return; + } + } + groups.push([text]); + }); + + return groups; + } + + function removeDuplicateElements(page: Page, groups: Text[][]) { + groups.forEach(group => { + const firstText: Text = group[0]; + + for (let i = 1; i < group.length; i++) { + if (isDuplicate(group[i], firstText)) { + const index: number = page.elements.indexOf(group[i], 0); + + if (index > -1) { + page.elements.splice(index, 1); + } + } + } + }); + } + + function isDuplicate(elem1: Text, elem2: Text): boolean { + return ( + elem1.toString() === elem2.toString() && + elem1.left === elem2.left && + elem1.top === elem2.top && + elem1.width === elem2.width && + elem1.height === elem2.height + // TODO check same font ('font' in elem1 && 'font' in elem2 && elem1['font'].isEqual(elem2['font'])) + ); + } + + /* + function tagRedundant(groups: Text[][]): Text[][] { + const redundant: Text[][] = []; + groups.forEach(group => { + if ( + group.length > doc.pages.length * opt.percentageOfRedondancy && + doc.pages.length > opt.minimumPages + ) { + group.forEach(t => (t.properties.isRedundant = true)); + redundant.push(group); + } + }); + + return redundant; + } + */ + } +} diff --git a/server/src/modules/RegexMatcherModule.ts b/server/src/modules/RegexMatcherModule.ts new file mode 100644 index 00000000..87e7c03f --- /dev/null +++ b/server/src/modules/RegexMatcherModule.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Paragraph, Word } from '../types/DocumentRepresentation'; +import { RegexMetadata } from '../types/Metadata'; +import logger from '../utils/Logger'; +import { LinesToParagraphModule } from './LinesToParagraphModule'; +import { Module } from './Module'; + +interface Options { + queries?: Array<{ regex: string; label: string }>; + isGlobal?: boolean; + isCaseSensitive?: boolean; +} + +const defaultOptions: Options = { + isCaseSensitive: true, + isGlobal: true, +}; + +export class RegexMatcherModule extends Module { + public static moduleName = 'regex-matcher'; + public static dependencies = [LinesToParagraphModule]; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + this.options.queries.forEach(query => { + logger.info(`Labeling Texts with label ${query.label} from regex ${query.regex}`); + + let regexType: string = ''; + if (this.options.isGlobal) { + regexType += 'g'; + } + if (this.options.isCaseSensitive) { + regexType += 'i'; + } + + const re: RegExp = new RegExp(query.regex, regexType); + doc.pages = doc.pages.map(page => { + // const labelCount = 0; + + const paragraphs = page.getElementsOfType(Paragraph); + for (const paragraph of paragraphs) { + let result = null; + // tslint:disable-next-line:no-conditional-assignment + while ((result = re.exec(paragraph.toString()))) { + const matchingWords: Word[] = paragraph.findWordsFromParagraphSubstring( + result.index, + result[0].length, + ); + + const metadata = new RegexMetadata(matchingWords, { + name: query.label, + regex: query.regex, + fullMatch: result[0], + groups: result.slice(1), + }); + + matchingWords.forEach(word => word.metadata.push(metadata)); + } + } + return page; + }); + }); + + return doc; + } +} diff --git a/server/src/modules/RemoteModule.ts b/server/src/modules/RemoteModule.ts new file mode 100644 index 00000000..2c073f1f --- /dev/null +++ b/server/src/modules/RemoteModule.ts @@ -0,0 +1,53 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import axios, { AxiosResponse } from 'axios'; +import { JsonExporter } from '../exporters/JsonExporter'; +import { Document, JsonExport } from '../types/DocumentRepresentation'; +import { json2document } from '../utils/json2document'; +import { Module } from './Module'; + +interface Options { + url?: string; + granularity?: string; +} + +const defaultOptions: Options = { + url: 'localhost', + granularity: 'word', +}; + +export class RemoteModule extends Module { + public static moduleName = 'remote'; + + constructor(options: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Promise { + const jsonExporter = new JsonExporter(doc, this.options.granularity); + const json: JsonExport = jsonExporter.getJson(); + + return axios({ + method: 'POST', + url: this.options.url, + data: json, + timeout: 0x7ffffff, + }).then((response: AxiosResponse) => { + return json2document(response.data); + }); + } +} diff --git a/server/src/modules/SeparateWordsModule.ts b/server/src/modules/SeparateWordsModule.ts new file mode 100644 index 00000000..80c99523 --- /dev/null +++ b/server/src/modules/SeparateWordsModule.ts @@ -0,0 +1,197 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Word } from '../types/DocumentRepresentation'; +import { Module } from './Module'; + +import * as englishDictArray from 'an-array-of-english-words'; + +/** + * Template Module. Do not use as is. + */ +export class SeparateWordsModule extends Module { + // The module name is useful to call it in the configuration. + // Please keep it kebab-case. + public static moduleName = 'separate-words'; + private MEMORY_SENTENCE: any = {}; + private englishDict: any = {}; + + // The main function will be called by the platform (by `../Cleaner.ts`). + public main(doc: Document): Document { + this.init(); + + doc.pages.forEach(p => + p.getElementsOfType(Word).forEach(word => { + if (word.toString().length > 8 && !this.englishDict[word.toString().toLowerCase()]) { + // Hack with dot to make it works + const newStr = this.addSpaces(word.toString() + '.'); + word.content = newStr.slice(0, newStr.length - 1); + } + }), + ); + + return doc; + } + + private init() { + for (const word of englishDictArray) { + this.englishDict[word] = 1; + } + + this.englishDict.abc = 1; + this.englishDict.axa = 1; + this.englishDict.allianz = 1; + this.englishDict.chubb = 1; + this.englishDict['’s'] = 1; + this.englishDict["'s"] = 1; + } + + private isNumber(str) { + const numberList = '0123456789'; + if (str.trim().length === 0) { + return false; + } + + for (const s of str) { + if (numberList.indexOf(s) === -1) { + return false; + } + } + return true; + } + + private breakWords(str) { + if (!str) { + return 'end'; + } + if (this.MEMORY_SENTENCE[str]) { + return this.MEMORY_SENTENCE[str]; + } + + const sentences = []; + + const punctuationsRegex = /\W+/; + for (let i = 0; i < str.length; ++i) { + if (punctuationsRegex.test(str[i])) { + const bestSub = this.breakWords(str.substr(i + 1)); + if (bestSub === 'end' || !bestSub) { + sentences.push({ + score: 10, + word: str[i], + }); + } else if (bestSub) { + sentences.push({ + sub: bestSub, + score: 10 + bestSub.score, + word: str[i], + }); + } + + break; + } + + if ( + this.englishDict[str.substr(0, i + 1).toLowerCase()] || + (this.isNumber(str.substr(0, i + 1)) && + (!this.isNumber(str.substr(0, i + 2)) || i + 2 >= str.length)) + ) { + const bestSub = this.breakWords(str.substr(i + 1)); + + const wordScore = (i + 1) * (i + 1) * (i + 1); + if (bestSub === 'end' || !bestSub) { + sentences.push({ + score: wordScore, + word: str.substr(0, i + 1), + }); + } else if (bestSub) { + sentences.push({ + sub: bestSub, + score: wordScore + bestSub.score, + word: str.substr(0, i + 1), + }); + } + } + } + + let bestScore = -1; + let bestSentence = null; + + for (const sentence of sentences) { + if (sentence.score > bestScore) { + bestSentence = sentence; + bestScore = sentence.score; + } + } + this.MEMORY_SENTENCE[str] = bestSentence; + return bestSentence; + } + + private addSpaces(str: string): string { + // console.log('SPACE', str); + // do not break urls + if (str.indexOf('www') === 0 || str.indexOf('http:') === 0) { + return str; + } + + // do not break @ + if (str.indexOf('@') !== -1 && str.indexOf('.') !== -1) { + return str; + } + + const punctuations = '.,,(();?!-#@£$€¥元/$=*+:'; + + const subSentences = []; + + for (let i = 0; i < str.length; i++) { + if (punctuations.indexOf(str[i]) !== -1) { + if (i > 0) { + subSentences.push(str.substr(0, i)); + } + + subSentences.push(str[i]); + str = str.substr(i + 1); + i = -1; + } + } + + let out = ''; + for (const subSentence of subSentences) { + if (punctuations.indexOf(subSentence[0]) !== -1) { + out += subSentence[0]; + continue; + } + let sentences = this.breakWords(subSentence); + while (sentences) { + if (sentences.word) { + if (out) { + out += ' '; + } + out += sentences.word; + } + sentences = sentences.sub; + } + } + + return out; + } +} + +// console.log(addSpaces('UnoccupiedHomesSecurityandHeating')); +// console.log(addSpaces('AmountofcoverforYourContents')); +// console.log(addSpaces("ying,orengaginginaerialactivitiesotherthanasapassengerinanaircraft")); +// console.log(addSpaces("Anyamountover£2,000,000forallcompensationandclaimant’s")); +// console.log(addSpaces(":FederalLawNo.(6)of2007concerningtheestablishment\ +// ofInsuranceAuthorityandOrganizationofitswork.Agent:")); diff --git a/server/src/modules/TemplateModule.ts b/server/src/modules/TemplateModule.ts new file mode 100644 index 00000000..b944414d --- /dev/null +++ b/server/src/modules/TemplateModule.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document } from '../types/DocumentRepresentation'; +import logger from '../utils/Logger'; +import { Module } from './Module'; +import { WordsToLineModule } from './WordsToLineModule'; + +// List of every options you need. +// Don't forget the question mark! +interface Options { + yourOption?: string; +} + +// Default options if none have been set in the configuration file. +const defaultOptions: Options = { + yourOption: 'hello world', +}; + +/** + * Template Module. Do not use as is. + */ +export class TemplateModule extends Module { + // The module name is useful to call it in the configuration. + // Please keep it kebab-case. + public static moduleName = 'template-module'; + // If your module can only be ran after another module, add it to the list. + // For instance, if your module needs a document where lines have already be created, + // you need to add `WordsToLineModule` as a dependency. + public static dependencies = [WordsToLineModule]; + + // This constructor ensures options and default options will be correctly copied. + constructor(options?: Options) { + super(options, defaultOptions); + } + + // The main function will be called by the platform (by `../Cleaner.ts`). + public main(doc: Document): Document { + // Modify the document here as you want. + // You can use options with `this.options.yourOption` + logger.info(this.options.yourOption); + return doc; + } + + // Note that you can also return a `Promise` if your process is async. + // In this case, use this main function instead: + /* + public main(doc: Document): Promise { + const promise: Promise = new Promise((resolve, reject) => { + resolve(doc); + }); + + return promise; + } + */ +} diff --git a/server/src/modules/WhitespaceRemovalModule.ts b/server/src/modules/WhitespaceRemovalModule.ts new file mode 100644 index 00000000..21f1761f --- /dev/null +++ b/server/src/modules/WhitespaceRemovalModule.ts @@ -0,0 +1,70 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Document, Page, Text } from '../types/DocumentRepresentation'; +import { Module } from './Module'; + +interface Options { + minWidth?: number; +} + +const defaultOptions: Options = { + minWidth: 0, +}; + +/** + * Stabiltiy: Stable + * Remove any text block that contains nothing but whitespace. + */ +export class WhitespaceRemovalModule extends Module { + public static moduleName = 'whitespace-removal'; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + doc.pages.forEach(page => { + page.elements = page.elements.filter(e => { + return ( + !(e instanceof Text) || + (e.width < this.options.minWidth || + (!/^\s*$/.test(e.toString()) && !isOverlapping(e, page))) + ); + }); + }); + + // Remove any space that are overlapping with text + // This is a weird but common case + function isOverlapping(text1: Text, page: Page): boolean { + const pageTexts = page.getTexts(); + for (const text2 of pageTexts) { + if ( + text1 !== text2 && + text1.top === text2.top && + text1.left === text2.left && + /^[ \t]*$/.test(text1.toString()) + ) { + return true; + } + } + + return false; + } + + return doc; + } +} diff --git a/server/src/modules/WordsToLineModule.ts b/server/src/modules/WordsToLineModule.ts new file mode 100644 index 00000000..db024dce --- /dev/null +++ b/server/src/modules/WordsToLineModule.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox, Document, Element, Line, Text, Word } from '../types/DocumentRepresentation'; +import * as utils from '../utils'; +import logger from '../utils/Logger'; +import { Module } from './Module'; +import { ReadingOrderDetectionModule } from './ReadingOrderDetectionModule'; + +interface Options { + lineHeightUncertainty?: number; + topUncertainty?: number; + maximumSpaceBetweenWords?: number; + mergeTableElements?: boolean; +} + +const defaultOptions: Options = { + lineHeightUncertainty: 1.2, // proportion + topUncertainty: 0.4, // proportion + maximumSpaceBetweenWords: 100, // value in px + mergeTableElements: false, +}; + +/** + * Stability: Stable + * Merge text block that are side by side to make lines. + */ +export class WordsToLineModule extends Module { + public static moduleName = 'words-to-line'; + public static dependencies = [ReadingOrderDetectionModule]; + + constructor(options?: Options) { + super(options, defaultOptions); + } + + public main(doc: Document): Document { + const opt: Options = {}; + Object.assign(opt, defaultOptions, this.options); + + doc.pages = doc.pages.map(page => { + const toBeMerged: Word[][] = []; + + if (page.getElementsOfType(Line).length > 0) { + logger.warn('Warning: this page already has some line in it. Not performing line merge.'); + return page; + } + + const words: Word[] = page + .getElementsOfType(Word) + .filter(Element.hasBoundingBox) + .sort(utils.sortElementsByOrder); + const otherElements: Element[] = page.elements.filter( + element => !(element instanceof Word) || !words.includes(element), + ); + + for (let i = 0; i < words.length; i++) { + const first = words[i]; + const mergeGroup: Word[] = [first]; + + for (let j = i + 1; j < words.length; j++) { + const prev = words[j - 1]; + const curr = words[j]; + + if ( + Math.abs(prev.top - curr.top) <= prev.height * opt.topUncertainty && + Math.abs(prev.height - curr.height) <= prev.height * opt.lineHeightUncertainty && + curr.left - (prev.left + prev.width) <= opt.maximumSpaceBetweenWords && + // FIXME element cannot be a Heading since it is a Word + // (prev instanceof Heading) === (curr instanceof Heading) && + prev.properties.isPageNumber === curr.properties.isPageNumber + // TODO: handle table elements: (opt.mergeTableElements + // || (!prev.metadata.tableElement && !curr.metadata.tableElement)) + ) { + mergeGroup.push(curr); + i++; + } else { + break; + } + } + + toBeMerged.push(mergeGroup); + } + + const texts: Text[] = []; + let newOrder = 0; + + toBeMerged.forEach((group: Word[]) => { + const line: Line = utils.mergeElements( + new Line(new BoundingBox(0, 0, 0, 0)), + ...group, + ); + line.properties.order = newOrder++; + texts.push(line); + }); + + // FIXME I think this will remove any chars left in the page + page.elements = otherElements.concat(texts); + + return page; + }); + + return doc; + } +} diff --git a/server/src/tslint.conf b/server/src/tslint.conf new file mode 100644 index 00000000..cd933db7 --- /dev/null +++ b/server/src/tslint.conf @@ -0,0 +1,47 @@ +{ + "extends": ["tslint:recommended", "tslint-config-airbnb", "tslint-eslint-rules"], + "rules": { + "object-curly-spacing": [ + "error", + "always" + ], + "array-element-newline": [ + "error", { + "minItems": 3 + } + ], + "indent": [true, "spaces", 2], + "ter-arrow-parens": false, + "max-line-length": [true, 200], + "no-console": [false], + "no-use-before-declare": false, + "ter-arrow-body-style": [ + 2, + "as-needed" + ], + "array-type": [true, "generic"], + "prefer-array-literal": [false], + "ban-types": [true, + ["Object", "Avoid using the `Object` type. Did you mean `object`?"], + ["Boolean", "Avoid using the `Boolean` type. Did you mean `boolean`?"], + ["Number", "Avoid using the `Number` type. Did you mean `number`?"], + ["String", "Avoid using the `String` type. Did you mean `string`?"], + ["Symbol", "Avoid using the `Symbol` type. Did you mean `symbol`?"]], + "object-literal-sort-keys": [false], + "interface-over-type-literal": [true], + "interface-name": [false], + "no-string-literal": [false], + "no-boolean-literal-compare": [true], + "import-name": [false], + "variable-name": [true, "ban-keywords", "check-format", "allow-pascal-case", "allow-leading-underscore"], + "member-access": [false], + "space-in-parens": [true, "never"], + "no-multi-spaces": [true], + "no-empty-interface": [false], + "no-increment-decrement": [false], + "forin": [false], + "align": [false], + "no-reference": [false], + "no-empty": [false] + } +} diff --git a/server/src/types/Config.ts b/server/src/types/Config.ts new file mode 100644 index 00000000..2c3a6cd1 --- /dev/null +++ b/server/src/types/Config.ts @@ -0,0 +1,228 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class Config { + public version: number; + public cleaner: CleanerConfig; + public extractor: ExtractorConfig; + public output: OutputConfig; + constructor(config: any) { + this.version = config.version; + this.cleaner = config.cleaner; + this.extractor = config.extractor; + this.output = config.output; + + if (typeof this.extractor.pdf === 'undefined') { + this.extractor.pdf = 'pdf2json'; + } + + if (typeof this.extractor.img === 'undefined') { + this.extractor.img = 'tesseract'; + } + + if (typeof this.output.granularity === 'undefined') { + this.output.granularity = 'word'; + } + } +} + +export type OutputGranularityOptions = 'character' | 'word'; +export interface OutputConfig { + granularity: OutputGranularityOptions; + includeMarginals: boolean; + formats: { + json?: boolean; + // 'json-compact'?: boolean; + text?: boolean; + markdown?: boolean; + // xml?: boolean; + // confidences?: boolean; + csv?: boolean; + pdf?: boolean; + }; +} + +export type CleanerConfig = Array; + +export interface ExtractorConfig { + pdf: 'pdf2json' | 'tesseract' | 'abbyy'; + img: 'tesseract' | 'abbyy'; + language: TesseractLanguage | TesseractLanguage[]; +} + +type TesseractLanguage = + | 'afr' + | 'amh' + | 'ara' + | 'asm' + | 'aze' + | 'aze_cyrl' + | 'bel' + | 'ben' + | 'bod' + | 'bos' + | 'bre' + | 'bul' + | 'cat' + | 'ceb' + | 'ces' + | 'chi_sim' + | 'chi_sim_vert' + | 'chi_tra' + | 'chi_tra_vert' + | 'chr' + | 'cos' + | 'cym' + | 'dan' + | 'deu' + | 'div' + | 'dzo' + | 'ell' + | 'eng' + | 'enm' + | 'epo' + | 'est' + | 'eus' + | 'fao' + | 'fas' + | 'fil' + | 'fin' + | 'fra' + | 'frk' + | 'frm' + | 'fry' + | 'gla' + | 'gle' + | 'glg' + | 'grc' + | 'guj' + | 'hat' + | 'heb' + | 'hin' + | 'hrv' + | 'hun' + | 'hye' + | 'iku' + | 'ind' + | 'isl' + | 'ita' + | 'ita_old' + | 'jav' + | 'jpn' + | 'jpn_vert' + | 'kan' + | 'kat' + | 'kat_old' + | 'kaz' + | 'khm' + | 'kir' + | 'kmr' + | 'kor' + | 'kor_vert' + | 'lao' + | 'lat' + | 'lav' + | 'lit' + | 'ltz' + | 'mal' + | 'mar' + | 'mkd' + | 'mlt' + | 'mon' + | 'mri' + | 'msa' + | 'mya' + | 'nep' + | 'nld' + | 'nor' + | 'oci' + | 'ori' + | 'osd' + | 'pan' + | 'pol' + | 'por' + | 'pus' + | 'que' + | 'ron' + | 'rus' + | 'san' + | 'script/Arabic' + | 'script/Armenian' + | 'script/Bengali' + | 'script/Canadian_Aboriginal' + | 'script/Cherokee' + | 'script/Cyrillic' + | 'script/Devanagari' + | 'script/Ethiopic' + | 'script/Fraktur' + | 'script/Georgian' + | 'script/Greek' + | 'script/Gujarati' + | 'script/Gurmukhi' + | 'script/HanS' + | 'script/HanS_vert' + | 'script/HanT' + | 'script/HanT_vert' + | 'script/Hangul' + | 'script/Hangul_vert' + | 'script/Hebrew' + | 'script/Japanese' + | 'script/Japanese_vert' + | 'script/Kannada' + | 'script/Khmer' + | 'script/Lao' + | 'script/Latin' + | 'script/Malayalam' + | 'script/Myanmar' + | 'script/Oriya' + | 'script/Sinhala' + | 'script/Syriac' + | 'script/Tamil' + | 'script/Telugu' + | 'script/Thaana' + | 'script/Thai' + | 'script/Tibetan' + | 'script/Vietnamese' + | 'sin' + | 'slk' + | 'slv' + | 'snd' + | 'snum' + | 'spa' + | 'spa_old' + | 'sqi' + | 'srp' + | 'srp_latn' + | 'sun' + | 'swa' + | 'swe' + | 'syr' + | 'tam' + | 'tat' + | 'tel' + | 'tgk' + | 'tha' + | 'tir' + | 'ton' + | 'tur' + | 'uig' + | 'ukr' + | 'urd' + | 'uzb' + | 'uzb_cyrl' + | 'vie' + | 'yid' + | 'yor'; diff --git a/server/src/types/DocumentRepresentation/Barcode.ts b/server/src/types/DocumentRepresentation/Barcode.ts new file mode 100644 index 00000000..01c231e8 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Barcode.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; + +/** + * Class representing a barcode entity, including a type of a barcode, as well as the content + * represented by it in the form of a string of characters. + */ +export class Barcode extends Element { + private _type: string; + private _content: string; + + constructor(boundingBox: BoundingBox, type?: string, content?: string) { + super(boundingBox); + this.type = type; + this.content = content; + } + + /** + * Getter type + * @return {string} + */ + public get type(): string { + return this._type; + } + + /** + * Getter content + * @return {string} + */ + public get content(): string { + return this._content; + } + + /** + * Setter type + * @param {string} value + */ + public set type(value: string) { + this._type = value; + } + + /** + * Setter content + * @param {string} value + */ + public set content(value: string) { + this._content = value; + } +} diff --git a/server/src/types/DocumentRepresentation/BoundingBox.ts b/server/src/types/DocumentRepresentation/BoundingBox.ts new file mode 100644 index 00000000..125b7ae4 --- /dev/null +++ b/server/src/types/DocumentRepresentation/BoundingBox.ts @@ -0,0 +1,160 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * BoundingBox represents the size as well the location of any DocumentRepresentation + * element, using the elements height, width, left and top. Other than a regular constructor, + * the bounding box can also be constructed using a merge of multiple existing bounding box types. + */ +export class BoundingBox { + /** + * Getter left + * @return {number} + */ + public get left(): number { + return this._left; + } + + /** + * Getter top + * @return {number} + */ + public get top(): number { + return this._top; + } + + /** + * Getter width + * @return {number} + */ + public get width(): number { + return this._width; + } + + /** + * Getter height + * @return {number} + */ + public get height(): number { + return this._height; + } + + /** + * Setter left + * @param {number} value + */ + public set left(value: number) { + this._left = value; + } + + /** + * Setter top + * @param {number} value + */ + public set top(value: number) { + this._top = value; + } + + /** + * Setter width + * @param {number} value + */ + public set width(value: number) { + this._width = value; + } + + /** + * Setter height + * @param {number} value + */ + public set height(value: number) { + this._height = value; + } + + /** + * Getter right + * @return {number} + */ + public get right(): number { + return this.left + this.width; + } + + /** + * Setter right + * @param {number} value + */ + public set right(value: number) { + this.width = value - this.left; + } + + /** + * Getter bottom + * @return {number} + */ + public get bottom(): number { + return this.top + this.height; + } + + /** + * Setter bottom + * @param {number} value + */ + public set bottom(value: number) { + this.height = value - this.top; + } + + /** + * Merges a list of bounding boxes and returns a single one englobing all + * the others + * @param boxes list of bounding boxes to be merged + * @returns a bounding box resulting from the complex hull of all the + * bounding boxes described by 'boxes' + */ + public static merge(boxes: BoundingBox[]): BoundingBox { + if (boxes.length === 0) { + return new BoundingBox(0, 0, 0, 0); + } + + const top: number = Math.min(...boxes.map(l => l.top)); + const bottom: number = Math.max(...boxes.map(l => l.top + l.height)); + const height: number = bottom - top; + const left: number = Math.min(...boxes.map(l => l.left)); + const right: number = Math.max(...boxes.map(l => l.left + l.width)); + const width: number = right - left; + return new BoundingBox(left, top, width, height); + } + + private _left: number; + private _top: number; + private _width: number; + private _height: number; + + constructor(left: number, top: number, width: number, height: number) { + this.left = left; + this.top = top; + this.width = width; + this.height = height; + } + + /** + * Checks if the area of a bounding box is empty, by checking if either the height or the width + * of the bounding box is equal to 0. + * @returns true or false depending on weather the area of the bounding box is zero or not, respectively. + */ + public areaIsEmpty(): boolean { + return this.height === 0 || this.width === 0; + } +} diff --git a/server/src/types/DocumentRepresentation/Character.ts b/server/src/types/DocumentRepresentation/Character.ts new file mode 100644 index 00000000..545591ea --- /dev/null +++ b/server/src/types/DocumentRepresentation/Character.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Text } from './Text'; + +/** + * The Character class is the derived class of the more general Text class which represents a textual + * element in the Document Represenation set of classes. + */ +export class Character extends Text { + private _content: string; + + constructor(boundingBox: BoundingBox, content: string = '') { + super(boundingBox); + this.content = content; + } + + /** + * Returns the Character content as a string. + */ + public toString(): string { + return this.content; + } + + /** + * Getter content + * @return {string} + */ + public get content(): string { + return this._content; + } + + /** + * Setter content + * @param {string} value + */ + public set content(value: string) { + this._content = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Color.ts b/server/src/types/DocumentRepresentation/Color.ts new file mode 100644 index 00000000..3f26472c --- /dev/null +++ b/server/src/types/DocumentRepresentation/Color.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Type Color, which basically represents the color as a string. + */ +export type Color = string; diff --git a/server/src/types/DocumentRepresentation/Document.ts b/server/src/types/DocumentRepresentation/Document.ts new file mode 100644 index 00000000..146a5b20 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Document.ts @@ -0,0 +1,97 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as clone from 'clone'; +import { Element } from './Element'; +import { Page } from './Page'; + +type Margins = { + top: number; + left: number; + bottom: number; + right: number; +}; + +/** + * The Document class which represents a valid document in the Document Representation as a collection + * of Page elements. Other than that, the class also contains attributes pertaining to document wide information + * like the top, left, right and bottom margins, which are set by an external module named HeaderFooterDetection. + */ +export class Document { + /** + * Getter pages + * @return {Page[]} + */ + public get pages(): Page[] { + return this._pages; + } + + /** + * Getter margins + * @return {Margins} + */ + public get margins(): Margins { + return this._margins; + } + + /** + * Setter pages + * @param {Page[]} value + */ + public set pages(value: Page[]) { + this._pages = value; + } + + /** + * Setter margins + * @param {Margins} value + */ + public set margins(value: Margins) { + this._margins = value; + } + + /** + * Generates a valid Document object from an input json. + * @param json The input json from which the Document is to be constructed + */ + public static fromJson(json: Page[]): Document { + const copy = clone(json); + + return new Document(copy); + } + private _pages: Page[]; + private _margins: Margins; + + constructor(pages: Page[] = []) { + this.pages = pages; + this.margins = { top: -1, left: -1, bottom: -1, right: -1 }; + } + + /** + * Returns all the elements of a document, traversing all the pages + */ + public getAllElements(): Element[] { + return this.pages.map(p => p.getAllElements()).reduce((acc, val) => acc.concat(val), []); + } + + /** + * Return an element of a particular ID in the Document + * @param id the id of the element to be matched to find the corresponding element + */ + public getElementById(id: number): Element { + return this.getAllElements().find(x => x.id === id); + } +} diff --git a/server/src/types/DocumentRepresentation/Drawing.ts b/server/src/types/DocumentRepresentation/Drawing.ts new file mode 100644 index 00000000..3d41b7f8 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Drawing.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; +import { SvgShape } from './SvgShape'; + +/** + * The Drawing element for the Document Representation structure, which contains an SVG shape, + * along with its bounding box. + */ +export class Drawing extends Element { + private _content: SvgShape[]; + + constructor(boundingBox: BoundingBox, content?: SvgShape[]) { + super(boundingBox); + this.content = content; + } + + /** + * Getter content + * @return {SvgShape[]} + */ + public get content(): SvgShape[] { + return this._content; + } + + /** + * Setter content + * @param {SvgShape[]} value + */ + public set content(value: SvgShape[]) { + this._content = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Element.ts b/server/src/types/DocumentRepresentation/Element.ts new file mode 100644 index 00000000..a6891101 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Element.ts @@ -0,0 +1,215 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import logger from '../../utils/Logger'; +import { Metadata, Properties } from '../Metadata'; +import { BoundingBox } from './BoundingBox'; + +/** + * The abstract class Element, which represents the generalisation of all entities that constitute the + * content of a document. This abstract class englobes all the shared attributes of the content classes. + */ +export abstract class Element { + /** + * Getter content + * @return {Element[]} + */ + public abstract get content(): Element[] | string | undefined; + + /** + * Setter content + * @param {Element[]} value + */ + public abstract set content(value: Element[] | string | undefined); + + /** + * Getter id + * @return {number} + */ + public get id(): number { + return this._id; + } + + /** + * Setter id + * @param {id} value + */ + public set id(value: number) { + this._id = value; + } + + /** + * Getter parent + * @return {Element} + */ + public get parent(): Element { + return this._parent; + } + + /** + * Getter children + * @return {Element[]} + */ + public get children(): Element[] { + return this._children; + } + + /** + * Getter boundingBox + * @return {BoundingBox} + */ + public get box(): BoundingBox | undefined { + return this._box; + } + + /** + * Setter parent + * @param {Element} value + */ + public set parent(value: Element) { + this._parent = value; + } + + /** + * Setter children + * @param {Element[]} value + */ + public set children(value: Element[]) { + this._children = value; + } + + /** + * Setter boundingBox + * @param {BoundingBox} value + */ + public set box(value: BoundingBox) { + this._box = value; + } + + /** + * Getter metadata + * @return {Metadata[]} + */ + public get metadata(): Metadata[] { + return this._metadata; + } + + /** + * Setter metadata + * @param {Metadata[]} value + */ + public set metadata(value: Metadata[]) { + this._metadata = value; + } + + /** + * Getter properties + * @return {Properties} + */ + public get properties(): Properties { + return this._properties; + } + + /** + * Setter properties + * @param {Properties} value + */ + public set properties(value: Properties) { + this._properties = value; + } + + // Syntaxic sugars for getters and setters + public set left(value: number) { + this.box.left = value; + } + public get left(): number { + return this.box.left; + } + public set top(value: number) { + this.box.top = value; + } + public get top(): number { + return this.box.top; + } + public set width(value: number) { + this.box.width = value; + } + public get width(): number { + return this.box.width; + } + public set height(value: number) { + this.box.height = value; + } + public get height(): number { + return this.box.height; + } + public set right(value: number) { + this.box.right = value; + } + public get right(): number { + return this.box.right; + } + public set bottom(value: number) { + this.box.bottom = value; + } + public get bottom(): number { + return this.box.bottom; + } + + /** + * Check if a given element has a bounding box or not + * @param element The input element + * @returns true/false depending on weather or not the element has a bounding box. + */ + public static hasBoundingBox(element: T): boolean { + if (typeof element === 'undefined') { + logger.warn('undefined: ', JSON.stringify(element)); + return false; + } + + return ( + typeof element.box !== 'undefined' && + typeof element.left !== 'undefined' && + typeof element.top !== 'undefined' && + typeof element.width !== 'undefined' && + typeof element.height !== 'undefined' + ); + } + + /** + * Reset global ID counter. DO NOT USE except for testing purpose. + * Reseting IDs IS dangerous and WILL create inconsistencies. + */ + public static resetGlobalId() { + this.globalId = 1; + } + + private static globalId = 1; + private _id: number; + private _metadata: Metadata[]; + private _properties: Properties; + private _parent: Element; + private _children: Element[]; + private _box?: BoundingBox; + + constructor(box?: BoundingBox) { + this._id = Element.globalId++; + this.box = box; + this._metadata = []; + this._properties = {}; + this.children = []; + } +} diff --git a/server/src/types/DocumentRepresentation/Font.ts b/server/src/types/DocumentRepresentation/Font.ts new file mode 100644 index 00000000..179c78b9 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Font.ts @@ -0,0 +1,222 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Color } from './Color'; + +export interface FontOptions { + weight?: string; + isItalic?: boolean; + isUnderline?: boolean; + color?: Color; + url?: string; + scaling?: number; +} + +/** + * The Font class representing a font, including all the attiributes necessary to represent an instance + * of the format of text, including the font name, size, italics, weight, color, etc. + */ +export class Font { + private _name: string; + private _size: number; + private _weight: string; + private _isItalic: boolean; + private _isUnderline: boolean; + private _color: Color; + private _url?: string; + private _scaling?: number; + + constructor(name: string, size: number, options?: FontOptions) { + this.name = name; + this.size = size; + + if (typeof options === 'undefined') { + options = {}; + } + + if (options.weight) { + this.weight = options.weight; + } else { + this.weight = 'medium'; + } + + if (options.isItalic) { + this.isItalic = options.isItalic; + } else { + this.isItalic = false; + } + + if (options.isUnderline) { + this.isUnderline = options.isUnderline; + } else { + this.isUnderline = false; + } + + if (options.color) { + this.color = options.color; + } else { + this.color = '#000000'; + } + } + + /** + * Compares a given font's properties with the current font's properties, to check if they are equal. + * @param font The name of the target font to be compared to 'this'. + * @return true/false depending on weather or not the font specified as parameter is the same as the one + * in 'this'. + */ + public isEqual(font: Font): boolean { + if ( + font.name === this.name && + font.size === this.size && + font.weight === this.weight && + font.isItalic === this.isItalic && + font.isUnderline === this.isUnderline && + font.color === this.color + ) { + return true; + } + return false; + } + + /** + * Getter name + * @return {string} + */ + public get name(): string { + return this._name; + } + + /** + * Getter size + * @return {number} + */ + public get size(): number { + return this._size; + } + + /** + * Getter weight + * @return {string} + */ + public get weight(): string { + return this._weight; + } + + /** + * Getter isItalic + * @return {boolean} + */ + public get isItalic(): boolean { + return this._isItalic; + } + + /** + * Getter isUnderline + * @return {boolean} + */ + public get isUnderline(): boolean { + return this._isUnderline; + } + + /** + * Getter color + * @return {Color} + */ + public get color(): Color { + return this._color; + } + + /** + * Getter url + * @return {string} + */ + public get url(): string { + return this._url; + } + + /** + * Getter scaling + * @return {number} + */ + public get scaling(): number { + return this._scaling; + } + + /** + * Setter name + * @param {string} value + */ + public set name(value: string) { + this._name = value; + } + + /** + * Setter size + * @param {number} value + */ + public set size(value: number) { + this._size = value; + } + + /** + * Setter weight + * @param {string} value + */ + public set weight(value: string) { + this._weight = value; + } + + /** + * Setter isItalic + * @param {boolean} value + */ + public set isItalic(value: boolean) { + this._isItalic = value; + } + + /** + * Setter isUnderline + * @param {boolean} value + */ + public set isUnderline(value: boolean) { + this._isUnderline = value; + } + + /** + * Setter color + * @param {Color} value + */ + public set color(value: Color) { + this._color = value; + } + + /** + * Setter url + * @param {string} value + */ + public set url(value: string) { + this._url = value; + } + + /** + * Setter scaling + * @param {string} value + */ + public set scaling(value: number) { + this._scaling = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Heading.ts b/server/src/types/DocumentRepresentation/Heading.ts new file mode 100644 index 00000000..dce11795 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Heading.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Line } from './Line'; +import { Paragraph } from './Paragraph'; + +/** + * A derived class from Paragraph, used to represent headings in a document. + * The attributes level represents the level of the heading; 1 being the highest level. + */ +export class Heading extends Paragraph { + private _level: number; + + /** + * Getter level + * @return {number} + */ + public get level(): number { + return this._level; + } + + /** + * Setter level + * @param {number} value + */ + public set level(value: number) { + this._level = value; + } + + constructor(boundingBox: BoundingBox, content: Line[] = [], level: number = 0) { + super(boundingBox, content); + this.level = level; + } +} diff --git a/server/src/types/DocumentRepresentation/Image.ts b/server/src/types/DocumentRepresentation/Image.ts new file mode 100644 index 00000000..561c6279 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Image.ts @@ -0,0 +1,48 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; + +/** + * Image element represents an image in a document, with the url attribute representing the location of + * the image. + */ +export class Image extends Element { + /** + * Getter url + * @return {string} + */ + public get url(): string { + return this._url; + } + + /** + * Setter url + * @param {string} value + */ + public set url(value: string) { + this._url = value; + } + + public content: null = null; + private _url: string; + + constructor(boundingBox: BoundingBox, url?: string) { + super(boundingBox); + this.url = url; + } +} diff --git a/server/src/types/DocumentRepresentation/JsonExport.ts b/server/src/types/DocumentRepresentation/JsonExport.ts new file mode 100644 index 00000000..b0e51580 --- /dev/null +++ b/server/src/types/DocumentRepresentation/JsonExport.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Properties } from '../Metadata'; +import { Color } from './Color'; + +export interface JsonExport { + pages: JsonPage[]; + fonts: JsonFont[]; + metadata: JsonMetadata[]; +} + +export interface JsonPage { + box: JsonBox; + pageNumber: number; + elements: JsonElement[]; +} + +export interface JsonBox { + l: number; + t: number; + w: number; + h: number; +} + +export interface JsonElement { + id: number; + type: string; // TODO be more precise + box?: JsonBox; + content?: JsonElement[] | string; + font?: number; + url?: string; + codeType?: string; + codeValue?: string; + conf?: number; + fromX?: number; + fromY?: number; + toX?: number; + toY?: number; + thickness?: number; + rowspan?: number; + colspan?: number; + isOrdered?: boolean; + properties?: JsonProperties; + metadata?: number[]; + level?: number; +} + +export type JsonProperties = Properties; + +export interface JsonFont { + id: number; + name: string; + size: number; + weight: string; + isItalic: boolean; + isUnderline: boolean; + color: Color; + url?: string; + scaling?: number; +} + +export interface JsonMetadata { + id: number; + elements: number[]; + type: string; + value?: number; + data?: any; +} diff --git a/server/src/types/DocumentRepresentation/Line.ts b/server/src/types/DocumentRepresentation/Line.ts new file mode 100644 index 00000000..3de78008 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Line.ts @@ -0,0 +1,90 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Text } from './Text'; +import { Word } from './Word'; + +/** + * The Line represents a text class which contains a group of words representing a horizontal chain + * of consecutive words. It is to be noted that a Line may or may not be a sentence, as a sentence can span + * multiple physical Line objects, or, multiple sentences can coexist in a single Line object. + */ +export class Line extends Text { + private _content: Word[]; + private _language: string; + private _scaling: number; + + constructor(boundingBox: BoundingBox, content: Word[] = []) { + super(boundingBox); + this.content = content; + } + + public toString(): string { + return this.content + .map(w => w.toString().trim()) + .reduce((w1, w2) => w1 + ' ' + w2, '') + .trim(); + } + + /** + * Getter content + * @return {Word[]} + */ + public get content(): Word[] { + return this._content; + } + + /** + * Getter language + * @return {string} + */ + public get language(): string { + return this._language; + } + + /** + * Getter scaling + * @return {number} + */ + public get scaling(): number { + return this._scaling; + } + + /** + * Setter content + * @param {Word[]} value + */ + public set content(value: Word[]) { + this._content = value; + } + + /** + * Setter language + * @param {string} value + */ + public set language(value: string) { + this._language = value; + } + + /** + * Setter scaling + * @param {number} value + */ + public set scaling(value: number) { + this._scaling = value; + } +} diff --git a/server/src/types/DocumentRepresentation/List.ts b/server/src/types/DocumentRepresentation/List.ts new file mode 100644 index 00000000..28c2d940 --- /dev/null +++ b/server/src/types/DocumentRepresentation/List.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; +import { Paragraph } from './Paragraph'; + +/** + * The List element is a collection of paragraphs that represent a block of list items in a Document. + * The boolean attribute isOrdered distinguishes an ordered from an unordered (bulleted) list. + * The list's level in terms of text indentation can also be specified using the level class attribute. + */ +export class List extends Element { + private _content: Paragraph[]; + private _isOrdered: boolean; + private _level: number; + + constructor(boundingBox: BoundingBox, content?: Paragraph[], isOrdered?: boolean) { + super(boundingBox); + this._content = content; + this._isOrdered = isOrdered; + } + + public addParagraph(paragraph: Paragraph) { + this.content.push(paragraph); + this.box = BoundingBox.merge([this.box, paragraph.box]); + } + + /** + * Getter content + * @return {Paragraph[]} + */ + public get content(): Paragraph[] { + return this._content; + } + + /** + * Getter isOrdered + * @return {boolean} + */ + public get isOrdered(): boolean { + return this._isOrdered; + } + + /** + * Getter level + * @return {number} + */ + public get level(): number { + return this._level; + } + + /** + * Setter content + * @param {Paragraph[]} value + */ + public set content(value: Paragraph[]) { + this._content = value; + } + + /** + * Setter isOrdered + * @param {boolean} value + */ + public set isOrdered(value: boolean) { + this._isOrdered = value; + } + + /** + * Setter level + * @param {number} value + */ + public set level(value: number) { + this._level = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Page.ts b/server/src/types/DocumentRepresentation/Page.ts new file mode 100644 index 00000000..6bd4fd98 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Page.ts @@ -0,0 +1,281 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isInBox } from '../../utils'; +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; +import { Text } from './Text'; + +export type directionType = 'horizontal' | 'vertical'; +/** + * A page in a document is represented by the Page class, which contains a list of elements, vertical and horizontal + * occupancy areas, and a page number. + */ +export class Page { + // Syntaxic sugars for getters and setters + public set left(value: number) { + this.box.left = value; + } + public get left(): number { + return this.box.left; + } + public set top(value: number) { + this.box.top = value; + } + public get top(): number { + return this.box.top; + } + public set width(value: number) { + this.box.width = value; + } + public get width(): number { + return this.box.width; + } + public set height(value: number) { + this.box.height = value; + } + public get height(): number { + return this.box.height; + } + private _pageNumber: number; + private _elements: Element[]; + private _box: BoundingBox; + private _horizontalOccupancy: boolean[]; + private _verticalOccupancy: boolean[]; + + constructor(pageNumber: number, elements: Element[], boundingBox: BoundingBox) { + this.pageNumber = pageNumber; + this.elements = elements; + this.box = boundingBox; + this.horizontalOccupancy = []; + this.verticalOccupancy = []; + + this.computePageOccupancy(); + } + + /** + * Computes all horizontal and vertical page occupancies + */ + public computePageOccupancy() { + this.horizontalOccupancy = Array(Math.floor(this.box.height)).fill(false); + this.verticalOccupancy = Array(Math.floor(this.box.width)).fill(false); + + const horizontalBarriers: number[][] = this.getBarriers('horizontal'); + horizontalBarriers.forEach(a => { + for (let i = Math.floor(a[0]); i !== Math.floor(a[1]) + 1; ++i) { + if (!this.horizontalOccupancy[i]) { + this.horizontalOccupancy[i] = true; + } + } + }); + const verticalBarriers: number[][] = this.getBarriers('vertical'); + verticalBarriers.forEach(a => { + for (let i = Math.floor(a[0]); i !== Math.floor(a[1]) + 1; ++i) { + if (!this.verticalOccupancy[i]) { + this.verticalOccupancy[i] = true; + } + } + }); + } + + /** + * Return the coordinates of each of the elements in a given list. + * @param elements The list of elements for which locations need to be returned. + * @param returnCenters Boolean indicating if the centers of the elements should be returned as location. + */ + public getLocationOfElements(elements: Element[], returnCenters: boolean = false): number[][] { + if (returnCenters) { + return elements.map((elem: Element) => [ + elem.box.left + elem.box.width / 2, + elem.box.top + elem.box.height / 2, + ]); + } else { + return elements.map((elem: Element) => [elem.box.left, elem.box.top]); + } + } + + /** + * Return the subset of all the elements completely inside a rectangle defining a subset of the given page. + * @param box Elements of the subset should be inside this bounding box. + * @param textOnly Elements of the subset should only be the textual elements. + */ + public getElementsSubset(box: BoundingBox, textOnly: boolean = false): Element[] { + return this.elements + .filter(e => e instanceof Text || textOnly) + .filter(e => isInBox(e.box, box)); + } + + /** + * Get first level text elements only + * + * @return {Text[]} + */ + public getTexts(): Text[] { + return this.elements.filter(e => e instanceof Text) as Text[]; + } + + /** + * Get a list of elements of type in the current Page instance. Pre-ordered. + * + * @param type Type of the Element we want to list + * @return the list of matching Elements + */ + public getElementsOfType(type: new (...args: any[]) => T): T[] { + const result: T[] = new Array(); + this.preOrderTraversal((element: Element) => { + if (element instanceof type) { + result.push(element); + } + }); + return result; + } + + /** + * Get all the elements of this page + */ + public getAllElements(): Element[] { + const result: Element[] = new Array(); + this.preOrderTraversal((element: Element) => { + result.push(element); + }); + return result; + } + + /** + * Pre-order traversal, calling back when a node is traversed. + * + * @param preOrderCallback yield the Element. + */ + public preOrderTraversal(preOrderCallback: (element: Element) => void): void { + let stack: Element[] = Array.from(this.elements); + + while (stack.length > 0) { + const element = stack.shift(); + preOrderCallback(element); + + if (element.content && typeof element.content !== 'string' && element.content.length !== 0) { + stack = stack.concat(element.content); + } + } + } + + /** + * Getter horizontalOccupancy + * @return {boolean[]} + */ + public get horizontalOccupancy(): boolean[] { + return this._horizontalOccupancy; + } + + /** + * Setter horizontalOccupancy + * @param {boolean[]} value + */ + public set horizontalOccupancy(value: boolean[]) { + this._horizontalOccupancy = value; + } + + /** + * Getter verticalOccupancy + * @return {boolean[]} + */ + public get verticalOccupancy(): boolean[] { + return this._verticalOccupancy; + } + + /** + * Setter verticalOccupancy + * @param {boolean[]} value + */ + public set verticalOccupancy(value: boolean[]) { + this._verticalOccupancy = value; + } + + /** + * Getter elements + * @return {Element[]} + */ + public get elements(): Element[] { + return this._elements; + } + + /** + * Getter pageNumber + * @return {number} + */ + public get pageNumber(): number { + return this._pageNumber; + } + + /** + * Getter box + * @return {BoundingBox} + */ + public get box(): BoundingBox { + return this._box; + } + + /** + * Setter elements + * @param {Element[]} value + */ + public set elements(value: Element[]) { + this._elements = value; + } + + /** + * Setter pageNumber + * @param {number} value + */ + public set pageNumber(value: number) { + this._pageNumber = value; + } + + /** + * Setter box + * @param {BoundingBox} value + */ + public set box(value: BoundingBox) { + this._box = value; + } + + /** + * Returns an array of type [[start:number, end:number]], representing blocks of occupied space along a + * particular direction of the page. + * @param direction Direction along which (horizontal or vertical) the barriers should be returned + */ + private getBarriers(direction: directionType): number[][] { + let barriers: number[][] = []; + if (direction === 'horizontal') { + barriers = this.elements + .map((elem: Element) => elem.box) + .map((b: BoundingBox) => { + const start: number = b.top; + const end: number = b.top + b.height; + return [start, end]; + }); + } else { + barriers = this.elements + .map((elem: Element) => elem.box) + .map((b: BoundingBox) => { + const start: number = b.left; + const end: number = b.left + b.width; + return [start, end]; + }); + } + return barriers; + } +} diff --git a/server/src/types/DocumentRepresentation/Paragraph.ts b/server/src/types/DocumentRepresentation/Paragraph.ts new file mode 100644 index 00000000..d23a5f23 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Paragraph.ts @@ -0,0 +1,162 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Font } from './Font'; +import { Line } from './Line'; +import { Text } from './Text'; +import { Word } from './Word'; + +/** + * The Paragraph class represents a collection of lines, fused together to represent a block of text + * which potentially represents a symantic grouping. + */ +export class Paragraph extends Text { + private _content: Line[]; + private _language: string; + + constructor(boundingBox: BoundingBox, content: Line[] = []) { + super(boundingBox); + this.content = content; + } + + /** + * Converts the entire paragraph into a string form, with linebreaks between lines and spaces between + * words. + */ + public toString(): string { + const content: string[] = this.content.map(l => l.toString()); + if (content.length !== 0) { + return content.reduce((l1, l2) => l1 + ' ' + l2, '').trim(); // TODO better carriage return handling + } else { + return ''; + } + } + + /** + * Get every words from the paragraph in a flat array. + */ + public getWords(): Word[] { + return this.content.map(l => l.content).reduce((a, b) => [...a, ...b]); + } + + /** + * Get every words that compose a paragrah's substring. + * @param start Begining of the string + * @param length Length of the string + */ + public findWordsFromParagraphSubstring(start: number, length: number): Word[] { + const allWords: Word[] = this.getWords(); + + let startIndex: number = 0; + for (let i = 0; i < allWords.length; i++) { + if ( + allWords + .slice(0, i + 1) + .map(w => w.toString()) + .join(' ').length >= start + ) { + startIndex = i; + break; + } + } + + let endIndex: number = 0; + for (let i = startIndex; i < allWords.length; i++) { + if ( + allWords + .slice(startIndex, i + 1) + .map(w => w.toString()) + .join(' ').length >= length + ) { + endIndex = i; + break; + } + } + + return allWords.slice(startIndex, endIndex + 1); + } + + /** + * Returns the main font of the paragraph using a basket + voting mechanism. The most used font will be returned + * as a valid Font object. + */ + public getMainFont(): Font { + const fonts: Font[] = this.content + .map((line: Line) => { + return line.content.map((word: Word) => word.font); + }) + .reduce((a, b) => a.concat(b), []); + + const baskets: Font[][] = []; + + fonts.forEach((font: Font) => { + let basketFound: boolean = false; + baskets.forEach((basket: Font[]) => { + if (basket.length > 0 && basket[0].isEqual(font)) { + basket.push(font); + basketFound = true; + } + }); + + if (!basketFound) { + baskets.push([font]); + } + }); + + baskets.sort((a, b) => { + return b.length - a.length; + }); + + if (baskets.length > 0 && baskets[0].length > 0) { + return baskets[0][0]; + } else { + throw new Error(`No font found for paragraph id ${this.id}`); + } + } + + /** + * Getter content + * @return {Line[]} + */ + public get content(): Line[] { + return this._content; + } + + /** + * Getter language + * @return {string} + */ + public get language(): string { + return this._language; + } + + /** + * Setter content + * @param {Line[]} value + */ + public set content(value: Line[]) { + this._content = value; + } + + /** + * Setter language + * @param {string} value + */ + public set language(value: string) { + this._language = value; + } +} diff --git a/server/src/types/DocumentRepresentation/SvgLine.ts b/server/src/types/DocumentRepresentation/SvgLine.ts new file mode 100644 index 00000000..2101b8c4 --- /dev/null +++ b/server/src/types/DocumentRepresentation/SvgLine.ts @@ -0,0 +1,139 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { SvgShape } from './SvgShape'; + +export class SvgLine extends SvgShape { + /** + * Getter lineType + * @return {string} + */ + public get lineType(): string { + return this._lineType; + } + + /** + * Setter lineType + * @param {string} value + */ + public set lineType(value: string) { + this._lineType = value; + } + + /** + * Getter thickness + * @return {number} + */ + public get thickness(): number { + return this._thickness; + } + + /** + * Setter thickness + * @param {number} value + */ + public set thickness(value: number) { + this._thickness = value; + } + + /** + * Getter fromX + * @return {number} + */ + public get from_x(): number { + return this._fromX; + } + + /** + * Getter fromY + * @return {number} + */ + public get from_y(): number { + return this._fromY; + } + + /** + * Getter toX + * @return {number} + */ + public get to_x(): number { + return this._toX; + } + + /** + * Getter toY + * @return {number} + */ + public get toY(): number { + return this._toY; + } + + /** + * Setter fromX + * @param {number} value + */ + public set fromX(value: number) { + this._fromX = value; + } + + /** + * Setter fromY + * @param {number} value + */ + public set fromY(value: number) { + this._fromY = value; + } + + /** + * Setter toX + * @param {number} value + */ + public set toX(value: number) { + this._toX = value; + } + + /** + * Setter toY + * @param {number} value + */ + public set toY(value: number) { + this._toY = value; + } + public content: null = null; + private _lineType: string; + private _thickness: number; + private _fromX: number; + private _fromY: number; + private _toX: number; + private _toY: number; + + constructor( + bbox: BoundingBox, + thickness: number, + fromX: number, + fromY: number, + toX: number, + toY: number, + ) { + super(bbox); + this.thickness = thickness; + this.fromX = fromX; + this.fromY = fromY; + this.toX = toX; + this.toY = toY; + } +} diff --git a/server/src/types/DocumentRepresentation/SvgShape.ts b/server/src/types/DocumentRepresentation/SvgShape.ts new file mode 100644 index 00000000..4f11f34f --- /dev/null +++ b/server/src/types/DocumentRepresentation/SvgShape.ts @@ -0,0 +1,19 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from './Element'; + +export abstract class SvgShape extends Element {} diff --git a/server/src/types/DocumentRepresentation/Table.ts b/server/src/types/DocumentRepresentation/Table.ts new file mode 100644 index 00000000..ffd4fd1d --- /dev/null +++ b/server/src/types/DocumentRepresentation/Table.ts @@ -0,0 +1,327 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; +import { TableCell } from './TableCell'; +import { TableRow } from './TableRow'; + +/* table span type: array<[rowNb, colNb, colspan, rowspan]> + * ... where the array represents a collection of cells; coordinates represented + * by rowNb, colNb, having either their colspan or rowspan !== 0, + */ +export type SpanType = Array<[number, number, number, number]>; +export interface TableShapeType { + rows: number; + cols: number; + spans: SpanType; +} +export class Table extends Element { + /** + * Getter rows + * @return {TableRow[]} + */ + public get content(): TableRow[] { + return this._content; + } + + /** + * Setter rows + * @param {TableRow[]} value + */ + public set content(value: TableRow[]) { + this._content = value; + this.calculateShape(); // recalculate row at each row setting + } + + /** + * Getter shape + * @return {TableShapeType} + */ + public get shape(): TableShapeType { + return this._shape; + } + + /** + * Setter shape + * @param {TableShapeType} value + */ + public set shape(value: TableShapeType) { + this._shape = value; + } + private _content: TableRow[]; + private _shape: TableShapeType; + + constructor(rows: TableRow[], boundingBox?: BoundingBox) { + if (!boundingBox) { + boundingBox = BoundingBox.merge(rows.map(row => row.box)); + } + super(boundingBox); + this.content = rows; + } + /** + * Returns the cell of a table at rowIndex, cellIndex. + * @param {rowIndex} number + * @param {cellIndex} number + * @return {TableCell} + */ + public getCellAt(rowIndex: number, cellIndex: number): TableCell { + return this.getRowAt(rowIndex).content[cellIndex]; + } + + /** + * Returns the row of a table at rowIndex. + * @param {rowIndex} number + * @return {TableRow} + */ + public getRowAt(rowIndex: number): TableRow { + return this.content[rowIndex]; + } + + /** + * Returns the col of a table at rowIndex. + * @param {colIndex} number + * @return {TableCell[]} + */ + public getColAt(colIndex: number): TableCell[] { + const t: TableCell[] = []; + this.content.forEach(row => { + if (row.content.length === this.shape.cols) { + t.push(row.content[colIndex]); + } else { + let counter: number = 0; + for (let i = 0; i !== row.content.length; ++i) { + const cell: TableCell = row.content[i]; + if (counter !== colIndex) { + counter += cell.colspan; + } else if (counter > colIndex) { + t.push(row.content[i - 1]); + } else { + t.push(cell); + } + } + } + }); + return t; + } + + /** + * Returns a set of rows of a table from rowFrom to rowTo. + * @param {rowFrom} number + * @param {rowTo} number + * @return {TableRow[]} + */ + public getRowFromTo(rowFrom: number, rowTo: number): TableRow[] { + if (rowFrom > this.content.length - 1 || rowTo > this.content.length) { + return []; + } + if (rowFrom < rowTo) { + return this.content.slice(rowFrom, rowTo); + } else if (rowFrom === rowTo) { + return [this.getRowAt(rowFrom)]; + } else { + return []; + } + } + + /** + * Gets all the elements inside all the cells of a row + * @param {rowIndex} number + * @return {Element[]} + */ + public getAllElementsInRow(rowIndex: number): Element[] { + let e: Element[] = []; + const r: TableRow = this.getRowAt(rowIndex); + r.content.forEach(cell => { + e = [...e, ...cell.content]; + }); + return e; + } + + /** + * Slices at a row, returning the set of tables split at each rowIndex + * and the content of all elements at each rowIndex. + * @param {rowIndex} number + * @return {Element[]} + */ + public sliceHorizontally(rowIndex: number[]): Element[] { + let newElements: Element[] = []; + + rowIndex.forEach(rowNb => { + const e: Element[] = this.getAllElementsInRow(rowNb); + newElements = [...newElements, ...e]; + }); + const originalContainsZero: boolean = rowIndex.includes(0); + + // pad the list, uniquify it, then sort it + if (!originalContainsZero) { + rowIndex.unshift(0); + } + rowIndex.push(this.content.length); + const uniq = rowIndex.filter((item, i, ar) => { + return ar.indexOf(item) === i; + }); + + uniq.sort((n1, n2) => n1 - n2); + const rowIndexSorted: number[] = uniq; + + // extract sub-tables + for (let i = 0; i !== rowIndexSorted.length - 1; ++i) { + let rowFrom: number; + if (rowIndexSorted[i] === 0) { + if (!originalContainsZero) { + rowFrom = 0; + } else { + rowFrom = 1; + } + } else { + rowFrom = rowIndexSorted[i] + 1; + } + const rowTo: number = rowIndexSorted[i + 1]; + const t: Table = new Table(this.getRowFromTo(rowFrom, rowTo)); + if (t.content.length !== 0) { + newElements.push(t); + } + } + return newElements; + } + + /** + * Performs cleaning of the table splits table at ghost cells and returns a set of + * elements that better represent the information in the table. + * + * @return {Element[]} + */ + public cleanTable(): Element[] { + let e: Element[] = []; + // this.fuseRedundantCells() // TODO add other cleaning algos here + e = [...e, ...this.splitTableAtGhostRows()]; + return e; + } + + /** + * Fuses redundant cells of the table + */ + public fuseRedundantCells() { + return; + } + + /** + * Split the table at ghost rows, return resulting rows. A ghost row has a single column + * @return {Element[]} + */ + public splitTableAtGhostRows(): Element[] { + const ghostRows: number[] = []; + this.shape.spans.forEach(entry => { + if (this.content[entry[0]].content.length === 1 && entry[2] === this.shape.cols) { + ghostRows.push(entry[0]); + } + }); + if (ghostRows.length !== 0) { + return this.sliceHorizontally(ghostRows); + } else { + return [this]; + } + } + + /** + * Get table dimensions + */ + public getDimensions(): [number, number] { + let nRow: number = 0; + let maxCol: number = 0; + + for (const row of this.content) { + let nCol: number = 0; + let minRowspan: number = Infinity; + + for (const cell of row.content) { + nCol += cell.colspan; + minRowspan = Math.min(minRowspan, cell.rowspan); + } + nRow += minRowspan; + + maxCol = Math.max(maxCol, nCol); + } + + return [nRow, maxCol]; + } + + /** + * Transform the table to a bidimensional array + */ + public toArray(): string[][] { + const dim: [number, number] = this.getDimensions(); + const arr: string[][] = new Array(dim[0]) + .fill(undefined) + .map(() => new Array(dim[1]).fill(undefined)); + let nRow: number = 0; + + for (let i = 0; i < arr.length; i++) { + const row: TableRow = this.content[nRow]; + let nCol: number = 0; + let jumpLine: number = Infinity; + + for (let j = 0; j < arr[i].length; j++) { + if (typeof arr[i][j] === 'undefined') { + const cell: TableCell = row.content[nCol]; + + // hotfix + if (typeof cell === 'undefined') { + continue; + } + + for (let c = 0; c < cell.colspan; c++) { + for (let r = 0; r < cell.rowspan; r++) { + arr[i + r][j + c] = null; + } + } + + arr[i][j] = cell.content.toString().trim(); + jumpLine = Math.min(jumpLine, cell.rowspan); + + j += cell.colspan - 1; + nCol++; + } + } + + i += jumpLine - 1; + nRow++; + } + + return arr; + } + + private calculateShape(): void { + const rowsNb: number = this.content.length; + const colsNb: number = Math.max(...this.content.map(row => row.content.length)); + const spansNb: SpanType = []; + for (const i in this.content) { + const row = this.content[i]; + for (const j in row.content) { + const cell = row.content[j]; + if (cell.colspan !== 1 || cell.rowspan !== 1) { + spansNb.push([parseInt(i, 10), parseInt(j, 10), cell.colspan, cell.rowspan]); + } + } + } + this.shape = { + cols: colsNb, + rows: rowsNb, + spans: spansNb, + }; + } +} diff --git a/server/src/types/DocumentRepresentation/TableCell.ts b/server/src/types/DocumentRepresentation/TableCell.ts new file mode 100644 index 00000000..6ba5298e --- /dev/null +++ b/server/src/types/DocumentRepresentation/TableCell.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; + +export class TableCell extends Element { + private _content: Element[]; + private _rowspan: number; + private _colspan: number; + + constructor(boundingBox: BoundingBox, content?: Element[], rowspan?: number, colspan?: number) { + super(boundingBox); + this.content = content; + + if (rowspan) { + this.rowspan = rowspan; + } else { + this.rowspan = 1; + } + + if (colspan) { + this.colspan = colspan; + } else { + this.colspan = 1; + } + } + + /** + * Getter content + * @return {Element[]} + */ + public get content(): Element[] { + return this._content; + } + + /** + * Getter rowspan + * @return {number} + */ + public get rowspan(): number { + return this._rowspan; + } + + /** + * Getter colspan + * @return {number} + */ + public get colspan(): number { + return this._colspan; + } + + /** + * Setter content + * @param {Element[]} value + */ + public set content(value: Element[]) { + this._content = value; + } + + /** + * Setter rowspan + * @param {number} value + */ + public set rowspan(value: number) { + this._rowspan = value; + } + + /** + * Setter colspan + * @param {number} value + */ + public set colspan(value: number) { + this._colspan = value; + } +} diff --git a/server/src/types/DocumentRepresentation/TableRow.ts b/server/src/types/DocumentRepresentation/TableRow.ts new file mode 100644 index 00000000..c2dbabd9 --- /dev/null +++ b/server/src/types/DocumentRepresentation/TableRow.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Element } from './Element'; +import { TableCell } from './TableCell'; + +export class TableRow extends Element { + private _content: TableCell[]; + + constructor(cells: TableCell[], boundingBox: BoundingBox) { + super(boundingBox); + this.content = cells; + } + + /** + * Getter cells + * @return {TableCell[]} + */ + public get content(): TableCell[] { + return this._content; + } + + /** + * Setter cells + * @param {TableCell[]} value + */ + public set content(value: TableCell[]) { + this._content = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Text.ts b/server/src/types/DocumentRepresentation/Text.ts new file mode 100644 index 00000000..1ab1392e --- /dev/null +++ b/server/src/types/DocumentRepresentation/Text.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from './Element'; + +export abstract class Text extends Element { + private _redundant: boolean; + private _confidence: number; + + public abstract toString(): string; + public abstract get content(): Text[] | string; + public abstract set content(value: Text[] | string); + + /** + * Getter redundant + * @return {boolean} + */ + public get redundant(): boolean { + return this._redundant; + } + + /** + * Getter confidence + * @return {number} + */ + public get confidence(): number { + return this._confidence; + } + + /** + * Setter redundant + * @param {boolean} value + */ + public set redundant(value: boolean) { + this._redundant = value; + } + + /** + * Setter confidence + * @param {number} value + */ + public set confidence(value: number) { + this._confidence = value; + } +} diff --git a/server/src/types/DocumentRepresentation/Word.ts b/server/src/types/DocumentRepresentation/Word.ts new file mode 100644 index 00000000..3fc19444 --- /dev/null +++ b/server/src/types/DocumentRepresentation/Word.ts @@ -0,0 +1,111 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BoundingBox } from './BoundingBox'; +import { Character } from './Character'; +import { Font } from './Font'; +import { Text } from './Text'; + +export class Word extends Text { + private _content: Character[] | string; + private _language: string; + private _isInDictionary: boolean; + private _font: Font; + + constructor( + boundingBox: BoundingBox, + content: Character[] | string = [], + font: Font, + language: string = '', + ) { + super(boundingBox); + this.content = content; + this.font = font; + this.language = language; + } + + public toString(): string { + if (typeof this.content === 'string') { + return this.content.trim(); + } else { + return this.content.map(c => c.toString()).reduce((c1, c2) => c1 + c2, ''); + } + } + + /** + * Getter content + * @return {Character[] | string} + */ + public get content(): Character[] | string { + return this._content; + } + + /** + * Getter language + * @return {string} + */ + public get language(): string { + return this._language; + } + + /** + * Getter isInDictionary + * @return {boolean} + */ + public get isInDictionary(): boolean { + return this._isInDictionary; + } + + /** + * Setter content + * @param {Character[] | string} value + */ + public set content(value: Character[] | string) { + this._content = value; + } + + /** + * Setter language + * @param {string} value + */ + public set language(value: string) { + this._language = value; + } + + /** + * Setter isInDictionary + * @param {boolean} value + */ + public set isInDictionary(value: boolean) { + this._isInDictionary = value; + } + + /** + * Getter font + * @return {Font} + */ + public get font(): Font { + return this._font; + } + + /** + * Setter font + * @param {Font} value + */ + public set font(value: Font) { + this._font = value; + } +} diff --git a/server/src/types/DocumentRepresentation/index.ts b/server/src/types/DocumentRepresentation/index.ts new file mode 100644 index 00000000..82a008a0 --- /dev/null +++ b/server/src/types/DocumentRepresentation/index.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Document'; +export * from './BoundingBox'; +export * from './Character'; +export * from './Drawing'; +export * from './Element'; +export * from './Heading'; +export * from './Image'; +export * from './List'; +export * from './Page'; +export * from './Paragraph'; +export * from './Table'; +export * from './TableCell'; +export * from './TableRow'; +export * from './Text'; +export * from './Word'; +export * from './Font'; +export * from './Line'; +export * from './Barcode'; +export * from './JsonExport'; diff --git a/server/src/types/Metadata/ComplexMetadata.ts b/server/src/types/Metadata/ComplexMetadata.ts new file mode 100644 index 00000000..2ff1789c --- /dev/null +++ b/server/src/types/Metadata/ComplexMetadata.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from '../DocumentRepresentation'; +import { Metadata } from './Metadata'; + +export class ComplexMetadata implements Metadata { + /** + * Getter elements + * @return {Element[]} + */ + public get elements(): Element[] { + return this._elements; + } + + /** + * Setter elements + * @param {Element[]} value + */ + public set elements(value: Element[]) { + this._elements = value; + } + + /** + * Getter data + * @return {T} + */ + public get data(): T { + return this._data; + } + + /** + * Setter data + * @param {T} value + */ + public set data(value: T) { + this._data = value; + } + + private _elements: Element[]; + private _data: T; + + constructor(elements: Element[], data: T) { + this._elements = elements; + this._data = data; + } +} diff --git a/server/src/types/Metadata/KeyValueMetadata.ts b/server/src/types/Metadata/KeyValueMetadata.ts new file mode 100644 index 00000000..543d4795 --- /dev/null +++ b/server/src/types/Metadata/KeyValueMetadata.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from '../DocumentRepresentation/Element'; +import { ComplexMetadata } from './ComplexMetadata'; + +type KeyValueData = { + keyName: string; + keyElements: Element[]; + valueElements: Element[]; +}; + +export class KeyValueMetadata extends ComplexMetadata {} diff --git a/server/src/types/Metadata/Metadata.ts b/server/src/types/Metadata/Metadata.ts new file mode 100644 index 00000000..9ac7db92 --- /dev/null +++ b/server/src/types/Metadata/Metadata.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from '../DocumentRepresentation/Element'; + +export interface Metadata { + elements: Element[]; +} diff --git a/server/src/types/Metadata/NumberMetadata.ts b/server/src/types/Metadata/NumberMetadata.ts new file mode 100644 index 00000000..87fca38f --- /dev/null +++ b/server/src/types/Metadata/NumberMetadata.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Element } from '../DocumentRepresentation'; +import { Metadata } from './Metadata'; + +export class NumberMetadata implements Metadata { + /** + * Getter elements + * @return {Element[]} + */ + public get elements(): Element[] { + return this._elements; + } + + /** + * Setter elements + * @param {Element[]} value + */ + public set elements(value: Element[]) { + this._elements = value; + } + + /** + * Getter value + * @return {number} + */ + public get value(): number { + return this._value; + } + + /** + * Setter value + * @param {number} value + */ + public set value(value: number) { + this._value = value; + } + + private _value: number; + private _elements: Element[]; + + constructor(elements: Element[], value: number) { + this.value = value; + this.elements = elements; + } +} diff --git a/server/src/types/Metadata/Properties.ts b/server/src/types/Metadata/Properties.ts new file mode 100644 index 00000000..1e09bf84 --- /dev/null +++ b/server/src/types/Metadata/Properties.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Properties { + titleScores?: { + size: number; + weight: number; + color: number; + name: number; + italic: number; + underline: number; + }; + + isRedundant?: boolean; + isHeader?: boolean; + isFooter?: boolean; + isPageNumber?: boolean; + bulletList?: boolean; + order?: number; +} diff --git a/server/src/types/Metadata/RegexMetadata.ts b/server/src/types/Metadata/RegexMetadata.ts new file mode 100644 index 00000000..9934da18 --- /dev/null +++ b/server/src/types/Metadata/RegexMetadata.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComplexMetadata } from './ComplexMetadata'; + +type RegexData = { + regex: string; + fullMatch: string; + groups: string[]; + name: string; +}; + +export class RegexMetadata extends ComplexMetadata {} diff --git a/server/src/types/Metadata/index.ts b/server/src/types/Metadata/index.ts new file mode 100644 index 00000000..a4e5ea4c --- /dev/null +++ b/server/src/types/Metadata/index.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './NumberMetadata'; +export * from './ComplexMetadata'; +export * from './KeyValueMetadata'; +export * from './Metadata'; +export * from './RegexMetadata'; +export * from './Properties'; diff --git a/server/src/types/Pdf2JsonFont.ts b/server/src/types/Pdf2JsonFont.ts new file mode 100644 index 00000000..e7962a06 --- /dev/null +++ b/server/src/types/Pdf2JsonFont.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class Pdf2JsonFont { + public fontspec: string; + public size: number; + public family: string; + public color: string; +} diff --git a/server/src/types/Pdf2JsonPage.ts b/server/src/types/Pdf2JsonPage.ts new file mode 100644 index 00000000..c25a9b01 --- /dev/null +++ b/server/src/types/Pdf2JsonPage.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Pdf2JsonFont } from './Pdf2JsonFont'; +import { Pdf2JsonText } from './Pdf2JsonText'; + +export class Pdf2JsonPage { + public fonts: Pdf2JsonFont[]; + public number: number; + public pages: number; + public height: number; + public width: number; + public text: Pdf2JsonText[]; + + constructor(page: Pdf2JsonPage) { + this.fonts = page.fonts; + this.number = page.number; + this.pages = page.pages; + this.height = page.height; + this.width = page.width; + this.text = page.text; + } +} diff --git a/server/src/types/Pdf2JsonText.ts b/server/src/types/Pdf2JsonText.ts new file mode 100644 index 00000000..f936a982 --- /dev/null +++ b/server/src/types/Pdf2JsonText.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class Pdf2JsonText { + public top: number; + public left: number; + public width: number; + public height: number; + public font: number; + public data: string; +} diff --git a/server/src/types/TableInfo.ts b/server/src/types/TableInfo.ts new file mode 100644 index 00000000..3f3791b4 --- /dev/null +++ b/server/src/types/TableInfo.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class TableInfoPage { + trimbox_coordinates: Box; + tables_trimbox_coordinates: Box[]; + tables_mediabox_coordinates: Box[]; + mediabox_dims: Dim; + filename: string; + class_ids: number[]; + page_number: number; + scores: number[]; +} + +export class Box { + x1: number; + x2: number; + y1: number; + y2: number; +} + +export class Dim { + height: number; + width: number; +} diff --git a/server/src/types/TableReconstruction.ts b/server/src/types/TableReconstruction.ts new file mode 100644 index 00000000..5b918b5a --- /dev/null +++ b/server/src/types/TableReconstruction.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class TableReconstruction { + kpis: Object; + meta_info: string[]; + table: string[][]; + page_nb: number; + table_id: number; +} diff --git a/server/src/types/TsvElement.ts b/server/src/types/TsvElement.ts new file mode 100644 index 00000000..042b1d63 --- /dev/null +++ b/server/src/types/TsvElement.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// tslint:disable: variable-name + +export class TsvElement { + public level: number; + public page_num: number; + public block_num: number; + public par_num: number; + public line_num: number; + public word_num: number; + public left: number; + public top: number; + public width: number; + public height: number; + public conf: number; + public text: string; +} diff --git a/server/src/utils.ts b/server/src/utils.ts new file mode 100644 index 00000000..efa595d6 --- /dev/null +++ b/server/src/utils.ts @@ -0,0 +1,590 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { spawnSync } from 'child_process'; +import * as concaveman from 'concaveman'; +import * as crypto from 'crypto'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { inspect } from 'util'; +import { BoundingBox, Document, Element, Page, Text } from './types/DocumentRepresentation'; +import logger from './utils/Logger'; + +let mutoolImagesFolder: string = ''; +let mutoolExtractionFolder: string = ''; + +export function replaceObject( + doc: Document, + oldObj: T, + newObj: U, +): Document { + doc.pages.forEach((page: Page) => { + page.elements = page.elements.map((element: Element) => { + _replaceObject(element); + if (element === oldObj) { + element = newObj; + } + return element; + }); + }); + + return doc; + + function _replaceObject(element: Element) { + if (element.parent) { + if (element.parent === oldObj) { + element.parent = newObj; + } + } + + element.children = element.children.map((child: Element) => { + if (child === oldObj) { + child = newObj; + } + return child; + }); + + if (Array.isArray(element.content)) { + element.content = element.content.map((elem: Element) => { + if (elem === oldObj) { + elem = newObj; + } + + _replaceObject(elem); + return elem; + }); + } + } +} + +// Handle Windows convert.exe conflict. +export function getConvertPath(): string { + if (/^win/i.test(os.platform())) { + const where = spawnSync('where.exe', ['convert']); + let filepaths: string[] = where.stdout.toString().split(os.EOL); + filepaths = filepaths.filter( + filepath => !/System/.test(filepath) && filepath.trim().length > 0, + ); + + if (filepaths.length === 0) { + throw new Error('Cannot find ImageMagick convert tool. Are you sure it is installed?'); + } else { + return filepaths[0]; + } + } else { + return 'convert'; + } +} + +export function getMutoolImagesPrefix(): string { + return 'page'; +} + +export function getMutoolImagesFolder(): string { + if (!mutoolImagesFolder) { + mutoolImagesFolder = getTemporaryDirectory(); + } + return mutoolImagesFolder; +} + +export function getMutoolExtractionFolder(): string { + if (!mutoolExtractionFolder) { + mutoolExtractionFolder = getTemporaryDirectory(); + } + return mutoolExtractionFolder; +} + +export function getTemporaryDirectory(): string { + const randFoldername = `${os.tmpdir()}/${crypto.randomBytes(15).toString('hex')}`; + fs.mkdirSync(randFoldername); + return path.resolve(`${randFoldername}`); +} + +export function getTemporaryFile(extension: string): string { + const randFilename = `${os.tmpdir()}/${crypto.randomBytes(15).toString('hex') + extension}`; + return path.resolve(`${randFilename}`); +} + +/** + * Sort function to sort elements by order + */ +export function sortElementsByOrder(elem1: Element, elem2: Element): number { + const orderA: number = getOrder(elem1); + const orderB: number = getOrder(elem2); + return orderA - orderB; + + function getOrder(element: Element): number { + if (typeof element.properties.order !== 'undefined') { + return element.properties.order; + } else if (Array.isArray(element.content)) { + return element.content + .map((cont: Element) => getOrder(cont)) + .reduce((a, b) => Math.min(a, b), Infinity); + } else { + return Infinity; + } + } +} + +/** + * Make subcollections of a predetermined size based on a collection. + * @param collection The collection to be used as the basis for the division. + * @param subCollectionSize The size of the smaller subcollections to be made + */ +export function getSubCollections(collection: T[], subCollectionSize: number): T[][] { + if (subCollectionSize >= collection.length) { + return [collection]; + } + const max: number = collection.length; + const result: T[][] = []; + let j: number = 0; + for (j = subCollectionSize; j !== max + 1; ++j) { + const i: number = j - subCollectionSize; + result.push(collection.slice(i, j)); + } + return result; +} + +/** + * Merge paragraphs blocks together. + * Also handles properly bullet points. + * @param content Array of text block to be merged into a single one + */ +export function mergeElements(parent: U, ...content: T[]): U { + if (content.length === 0) { + return parent; + } + + content = content.filter(l => l !== null && typeof l !== 'undefined'); + content.sort(sortElementsByOrder); + + parent.content = content; + + parent.box = BoundingBox.merge(content.map(c => c.box)); + + // FIXME Add font support + // paragraph.font = (lines.sort((a, b) => b.data.length - a.data.length)[0] || paragraph).font; + + // TODO Find a clever way to handle that (or not) + // paragraph.metadata = utils.concatTags(...lines.map(l => l.metadata)); + + return parent; +} + +/** + * The "median" is the "middle" value in the list of numbers. + * + * @param {Array} numbers An array of numbers. + * @return {Number} The calculated median value from the specified numbers. + */ +export function median(numbers: number[]): number { + numbers.sort((a, b) => a - b); + + if (numbers.length % 2 === 0) { + return (numbers[numbers.length / 2 - 1] + numbers[numbers.length / 2]) / 2; + } else { + return numbers[(numbers.length - 1) / 2]; + } +} + +/** + * Check if text blocks are vertically aligned in the center. + */ +export function isAlignedCenter(texts: Text[], alignUncertainty: number = 0): boolean { + for (let i = 0; i < texts.length - 1; i++) { + const t1 = texts[i]; + const t2 = texts[i + 1]; + if (Math.abs(t1.left + t1.width / 2 - (t2.left + t2.width / 2)) > alignUncertainty) { + return false; + } + } + + return true; +} + +/** + * Check if text blocks are vertically aligned on the left or right side. + * Also handles bullet point with an uncertainty. + */ +export function isAligned( + texts: Text[], + alignUncertainty: number = 0, + bulletUncertainty: number = 40, +): boolean { + return ( + isAlignedLeft(texts, alignUncertainty, bulletUncertainty) || + isAlignedRight(texts, alignUncertainty) + ); +} + +export function isAlignedLeft( + texts: Text[], + alignUncertainty: number = 0, + bulletUncertainty: number = 40, +): boolean { + for (let i = 0; i < texts.length - 1; i++) { + const t1 = texts[i]; + const t2 = texts[i + 1]; + + // if (!t1.metadata.bulletList && !t2.metadata.bulletList) { + // bulletUncertainty = 0; + // } + + if (Math.abs(t1.left - t2.left) > alignUncertainty + bulletUncertainty) { + return false; + } + } + + return true; +} + +export function isAlignedRight(texts: Text[], alignUncertainty: number = 0): boolean { + for (let i = 0; i < texts.length - 1; i++) { + const t1 = texts[i]; + const t2 = texts[i + 1]; + + if (Math.abs(t1.left + t1.width - (t2.left + t2.width)) > alignUncertainty) { + return false; + } + } + + return true; +} + +export function isAlignedAndOverlapVertically(texts: Text[]): boolean { + if (texts.length === 0) { + return true; + } + + return ( + (isAligned(texts) || isAlignedCenter(texts)) && + texts.every(t => t.top === texts[0].top) && + texts.every(t => t.height === texts[0].height) + ); +} + +/** + * Check if an element is contained inside a bounding box + * @param element Element that'll be checked + * @param box Containing box + * @param strict Will check if the element can stay strictly in the box without overstepping (Default: `true`) + */ +export function isInBox(element: BoundingBox, box: BoundingBox, strict: boolean = true): boolean { + if (strict) { + return ( + element.top >= box.top && + element.top + element.height <= box.top + box.height && + element.left >= box.left && + element.left + element.width <= box.left + box.width + ); + } else { + return ( + element.top < box.top + box.height && + element.top + element.height > box.top && + element.left < box.left + box.width && + element.left + element.width > box.left + ); + } +} + +/** + * Check if blocks are in the same location, but maybe on different pages + * @param uncertainty error margin in px + * @param texts text block that'll be compared + */ +export function hasSameLocation(uncertainty: number, ...texts: Text[]): boolean { + const top = Math.min(...texts.map(t => t.top)); + const left = Math.min(...texts.map(t => t.left)); + const bottom = Math.max(...texts.map(t => t.top + t.height)); + const right = Math.max(...texts.map(t => t.left + t.width)); + + for (const t of texts) { + if ( + t.top > top + uncertainty || + t.left > left + uncertainty || + top + t.height < bottom - uncertainty * 2 || + left + t.width < right - uncertainty * 2 + ) { + return false; + } + } + + return true; +} + +/** + * Verifies if a string of text is a bullet point or not. + * @param text The input text to be checked. + * @returns true/false representing the result of the check. + */ +export function isBullet(text: Text): boolean { + const bulletCharacters: string[] = ['●', '', '•', '', 'º', '■', '–', '·', '*', '-']; + const bulletOr = bulletCharacters.map(b => `\\${b}`).join('|'); + return new RegExp(`^(${bulletOr})`).test(text.toString().trim()); +} + +/** + * Verifies if a string of text is a numbered list item or not. + * @param text The input text to be checked. + * @returns true/false representing the result of the check. + */ +export function isNumbering(text: Text): boolean { + const regex = /^\d[.)0-9]*/gm; + return regex.test(text.toString().trim()); +} + +/** + * Remove `null` or `undefined` elements + * @param doc + */ +export function removeNull(page: Page): Page { + const newElements: Element[] = page.elements.filter(e => e !== null && typeof e !== 'undefined'); + if (page.elements.length - newElements.length !== 0) { + logger.debug( + `Null elements removed for page #${page.pageNumber}: ${page.elements.length - + newElements.length}`, + ); + page.elements = newElements; + } + return page; +} + +/** + * Get page from page number + * @param doc Document + * @param pageNumber Page number + */ +export function getPage(doc: Document, pageNumber: number): Page { + return doc.pages.filter(p => p.pageNumber === pageNumber)[0]; +} + +/** + * Build a RegExp that matches any page numbers (i.e. Page 3, -3-, 3 of 5, (iii), etc.) + */ +export function getPageRegex(): RegExp { + const pageWord = '(?:Pages?|Páginas?)'; + const ofWord = '(?:of|de|-|/)'; + + const before = '[\\(\\[\\- ]*'; + const after = '[\\]\\)\\- ]*'; + + const arabNumber = '[\\d]+'; + const romanNumber = 'M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})'; + const pageNumber = `(${arabNumber}|${romanNumber})`; + + const pagePrefix = `${pageWord}\\s*(?:\\|\\s*)?`; + + const pageRegex = new RegExp( + `^(?:` + + `(?:${pagePrefix}${pageNumber})|` + + `(?:${pageNumber}\\s*(?:\\|\\s*)?${pageWord})|` + + `(?:(?:${pageWord}\\s*)?${pageNumber}\\s*${ofWord}\\s*${pageNumber})|` + + `(?:${before}${pageNumber}${after})` + + `)$`, + 'i', + ); + + return pageRegex; +} + +/** + * Create a pool of promises with a maximal number of concurrent executions + * @param poolLimit Max number of concurrent executions + * @param args Arguments to give to the promiseConstructor + * @param promiseConstructor Function that will create promises + */ +export function promisePool( + poolLimit: number, + args: U[], + promiseConstructor: (arg: U) => Promise, +): Promise { + return new Promise((resolve, reject) => { + let i: number = 0; + const allPromises: Array> = []; + const racingPromises: Array> = []; + + function enqueue(): void { + if (allPromises.length === args.length) { + // Every promise has been created, one just waits for them to resolve + Promise.all(allPromises) + .then(values => { + resolve(values); + }) + .catch(e => reject(e)); + } else { + // Create a new promise and add it to the running pool + const arg: U = args[i++]; + const promise: Promise = promiseConstructor(arg); + promise.then(() => racingPromises.splice(racingPromises.indexOf(promise), 1)); + allPromises.push(promise); + racingPromises.push(promise); + + if (racingPromises.length < poolLimit) { + enqueue(); + } else { + Promise.race(racingPromises) + .then(() => { + enqueue(); + }) + .catch(e => reject(e)); + } + } + } + + enqueue(); + }); +} + +/** + * Prettifies an object and returns the pretty string + * @param obj The object to be prettified + */ +export function prettifyObject(obj: object, compact: boolean = false): string { + return inspect(obj, { colors: true, compact }); +} + +export function round(n: number, decimals: number = 2): number { + return Math.round(n * Math.pow(10, decimals)) / Math.pow(10, decimals); +} + +export function toKebabCase(str: string): string { + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +} + +/** + * Generates a convex hull from vertices + * @param vertices the list of vertices of type number[][], as [[x,y], ...] + * @return polygon as number[][] + */ +export function getConvexHull(vertices: number[][]): number[][] { + return concaveman(vertices); +} + +/** + * Computes all the angles of a polygon + * @param orderedVertices list of vertices forming the segments as number[][] + * @return angles at each vertex as number[] + */ +export function getAnglesOfPolygon(orderedVertices: number[][]): number[] { + function computeAngle(dx: number, dy: number): number { + let theta = Math.atan2(dy, dx); // range (-PI, PI] + theta *= 180 / Math.PI; // rads to degs, range (-180, 180] + // if (theta < 0) theta = 360 + theta; // range [0, 360) + return theta; + } + const angles: number[] = []; + orderedVertices.forEach((vertex, index) => { + if (index !== orderedVertices.length - 1) { + const from: number[] = vertex; + const to: number[] = orderedVertices[index + 1]; + angles.push(computeAngle(to[0] - from[0], to[1] - from[1])); + } + }); + return angles; +} + +/** + * Computes addition of two vectors. If they're of different sizes, the extra + * dimentions are copied as-is at the trailing end of the result. + * @param vec1: first vector in n dimentions + * @param vec2: second vector in m dimentions + */ +export function addVectors(vec1: number[], vec2: number[]): number[] { + let smallerVector: number[]; + let biggerVector: number[]; + let result: number[] = []; + if (vec1.length < vec2.length) { + smallerVector = vec1; + biggerVector = vec2; + } else if (vec2.length < vec1.length) { + smallerVector = vec2; + biggerVector = vec1; + } else { + for (let i = 0; i !== vec1.length; ++i) { + result.push(vec1[i] + vec2[i]); + } + return result; + } + + for (let i = 0; i !== smallerVector.length; ++i) { + result.push(smallerVector[i] + biggerVector[i]); + } + + result = [...result, ...biggerVector.slice(smallerVector.length, biggerVector.length)]; + return result; +} + +/** + * Computes eucledian distance between two vectors. + * @param vec1: first vector in n dimentions + * @param vec2: second vector in n dimentions + */ +export function getEucledianDistance(vec1: number[], vec2: number[]): number { + if (vec1.length !== vec2.length) { + return -1; + } // maybe resize? TODO + const subtracted = vec1.map((i, n) => i - vec2[n]); + const powered = subtracted.map(e => Math.pow(e, 2)); + const sum = powered.reduce((total, current) => total + current, 0); + return Math.sqrt(sum); +} + +/** + * Computes the magnitude of a vector. + * @param vec: the vector + */ +export function getMagnitude(vec: number[]): number { + let sumOfSquares: number = 0; + for (const n of vec) { + sumOfSquares += n * n; + } + return Math.sqrt(sumOfSquares); +} + +/** + * Computes the dot product between two vectors. + * @param vec1: first vector in n dimentions + * @param vec2: second vector in n dimentions + */ +export function getDotProduct(vec1: number[], vec2: number[]): number { + let result: number = 0; + const lim: number = Math.min(vec1.length, vec2.length); + if (vec1.length !== vec2.length) { + logger.warn('[dotProduct] vectors have different sizes:', vec1.length, vec2.length); + logger.warn('[dotProduct] taking min size', lim); + } + for (let i = 0; i < lim; i++) { + result += vec1[i] * vec2[i]; + } + return result; +} + +/** + * Finds the occurance of an element in an array and returns the positions. + * @param array: an array of type T + * @param element: element to be looked for in the array + * @return an array of position(s) + */ +export function findPositionsInArray(array: T[], element: T): number[] { + const result: number[] = []; + array.forEach((value, position) => { + if (value === element) { + result.push(position); + } + }); + return result; +} diff --git a/server/src/utils/Logger.ts b/server/src/utils/Logger.ts new file mode 100644 index 00000000..822c2422 --- /dev/null +++ b/server/src/utils/Logger.ts @@ -0,0 +1,95 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as pino from 'pino'; + +/** + * Pino wrapper to be able to use commander options easily + */ +/* tslint:disable-next-line:class-name */ +class logger { + public static init(isPrettyLogs: boolean = false): void { + const options: any = { + name: 'parsr', + level: 'info', + }; + + if (isPrettyLogs) { + options.prettyPrint = { + colorize: true, + ignore: 'pid,hostname', + translateTime: "yyyy-mm-dd'T'HH:MM:ss", + }; + } + + this.pinoLogger = pino(options); + } + + public static getPinoLogger(): pino.Logger { + this.checkInit(); + return this.pinoLogger; + } + + public static trace(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.trace(msg, ...args); + } + + public static debug(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.debug(msg, ...args); + } + + public static info(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.info(msg, ...args); + } + + public static warn(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.warn(msg, ...args); + } + + public static error(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.error(msg, ...args); + } + + public static fatal(msg: string, ...args: any[]) { + this.checkInit(); + this.pinoLogger.fatal(msg, ...args); + } + + public static set level(level) { + this.checkInit(); + this.pinoLogger.level = level; + } + + public static get level() { + this.checkInit(); + return this.pinoLogger.level; + } + + private static pinoLogger: pino.Logger; + + private static checkInit() { + if (!this.pinoLogger) { + this.init(true); + } + } +} + +export default logger; diff --git a/server/src/utils/json2document.ts b/server/src/utils/json2document.ts new file mode 100644 index 00000000..a3b2abcf --- /dev/null +++ b/server/src/utils/json2document.ts @@ -0,0 +1,402 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Barcode, + BoundingBox, + Character, + Document, + Drawing, + Element, + Font, + Heading, + Image, + JsonElement, + JsonExport, + JsonFont, + JsonMetadata, + JsonPage, + JsonProperties, + Line, + List, + Page, + Paragraph, + Table, + TableCell, + TableRow, + Text, + Word, +} from '../types/DocumentRepresentation'; +import { FontOptions } from '../types/DocumentRepresentation/Font'; +import { SvgLine } from '../types/DocumentRepresentation/SvgLine'; +import { SvgShape } from '../types/DocumentRepresentation/SvgShape'; +import { KeyValueMetadata, Properties, RegexMetadata } from '../types/Metadata'; +import { prettifyObject } from '../utils'; +import logger from './Logger'; + +export function json2document(inputJson: JsonExport): Document { + const fonts: Font[] = []; + + const doc: Document = new Document([]); + constructFonts(inputJson.fonts, fonts); + constructPagesObj(inputJson.pages, doc.pages, fonts); + constructMetadataObj(inputJson.metadata, doc); + + return doc; +} + +function constructPagesObj(jsonPages: JsonPage[], outputPagesContainer: Page[], fonts: Font[]) { + jsonPages.forEach(page => { + const elementsDS: Element[] = []; + page.elements.forEach(e => { + const eDS = elementsFromJson(e, fonts); + if (typeof eDS !== 'undefined') { + elementsDS.push(eDS); + } + }); + if (elementsDS.length !== 0) { + outputPagesContainer.push( + new Page( + page.pageNumber, + elementsDS, + new BoundingBox(page.box.l, page.box.t, page.box.w, page.box.h), + ), + ); + } else { + logger.info('[JsonExtractor] didnt construct any elements', prettifyObject(elementsDS)); + } + }); +} + +function elementsFromJson(elementJson: JsonElement, fonts: Font[]): Element | void { + if (elementJson.type === 'table') { + return tableFromJson(elementJson, fonts); + } else if (elementJson.type === 'image') { + return imageFromJson(elementJson); + } else if (elementJson.type === 'list') { + return listFromJson(elementJson, fonts); + } else if (elementJson.type === 'drawing') { + return drawingFromJson(elementJson); + } else if (elementJson.type === 'barcode') { + return barcodeFromJson(elementJson); + } else if ( + elementJson.type === 'paragraph' || + elementJson.type === 'heading' || + elementJson.type === 'line' || + elementJson.type === 'word' || + elementJson.type === 'character' + ) { + return textFromJson(elementJson, fonts); + } else { + logger.warn( + '[JsonExtractor] Unknown element type in the JSON file: ', + elementJson, + '. Ignoring...', + ); + } +} + +function constructFonts(inputFonts: JsonFont[], fonts: Font[]) { + inputFonts.forEach(font => { + const options: FontOptions = {}; + + if (font.weight) { + options.weight = font.weight; + } + if (font.isItalic) { + options.isItalic = font.isItalic; + } + if (font.isUnderline) { + options.isUnderline = font.isUnderline; + } + if (font.color) { + options.color = font.color; + } + if (font.url) { + options.url = font.url; + } + if (font.scaling) { + options.scaling = font.scaling; + } + + const fontDS: Font = new Font(font.name, font.size, options); + fonts[font.id] = fontDS; + }); +} + +function constructMetadataObj(inputMetadata: JsonMetadata[], d: Document) { + inputMetadata.forEach(m => { + switch (m.type) { + case 'key-value': + const allElements1: Element[] = m.elements.map(e => d.getElementById(e)); + const md1: KeyValueMetadata = new KeyValueMetadata(allElements1, { + keyName: m.data.keyName, + keyElements: m.data.keyElements.map(e => d.getElementById(e)), + valueElements: m.data.valueElements.map(e => d.getElementById(e)), + }); + allElements1.forEach(e => e.metadata.push(md1)); + break; + case 'regex': + const allElements2: Element[] = m.elements.map(e => d.getElementById(e)); + const md2: RegexMetadata = new RegexMetadata(allElements2, { + regex: m.data.regkeyElemex, + fullMatch: m.data.fullMatch, + groups: m.data.groups, + name: m.data.name, + }); + allElements2.forEach(e => e.metadata.push(md2)); + break; + } + }); +} + +function propertiesFromJson(propertiesObj: JsonProperties): Properties { + const prop: Properties = {}; + if (propertiesObj.hasOwnProperty('titleScores')) { + prop.titleScores = { + size: propertiesObj.titleScores.size, + weight: propertiesObj.titleScores.weight, + color: propertiesObj.titleScores.color, + name: propertiesObj.titleScores.name, + italic: propertiesObj.titleScores.italic, + underline: propertiesObj.titleScores.underline, + }; + } + if (propertiesObj.hasOwnProperty('order')) { + prop.order = propertiesObj.order; + } else { + logger.info( + `the properties obj inputted does not have the order key: ${prettifyObject(propertiesObj)}`, + ); + } + if (propertiesObj.hasOwnProperty('isHeader')) { + prop.isHeader = propertiesObj.isHeader; + } + if (propertiesObj.hasOwnProperty('isFooter')) { + prop.isFooter = propertiesObj.isFooter; + } + if (propertiesObj.hasOwnProperty('isPageNumber')) { + prop.isPageNumber = propertiesObj.isPageNumber; + } + if (propertiesObj.hasOwnProperty('bulletList')) { + prop.bulletList = propertiesObj.bulletList; + } + return prop; +} + +function tableFromJson(tableObj: JsonElement, fonts: Font[]): Table { + const rowsDS: TableRow[] = []; + + if (Array.isArray(tableObj.content)) { + tableObj.content.forEach(rowObj => { + const cellsDS: TableCell[] = []; + + if (Array.isArray(rowObj.content)) { + rowObj.content.forEach(cellObj => { + if (Array.isArray(cellObj.content)) { + const content: Element[] = cellObj.content + .map(e => elementsFromJson(e, fonts)) + .filter(e => e instanceof Element) as Element[]; + const newCell: TableCell = new TableCell( + new BoundingBox(cellObj.box.l, cellObj.box.t, cellObj.box.w, cellObj.box.h), + content, + cellObj.rowspan, + cellObj.colspan, + ); + + newCell.id = cellObj.id; + newCell.properties = propertiesFromJson(cellObj.properties); + cellsDS.push(newCell); + } + }); + } + + const newRow: TableRow = new TableRow( + cellsDS, + new BoundingBox(rowObj.box.l, rowObj.box.t, rowObj.box.w, rowObj.box.h), + ); + + newRow.id = rowObj.id; + newRow.properties = propertiesFromJson(rowObj.properties); + rowsDS.push(newRow); + }); + } + + const newTable: Table = new Table( + rowsDS, + new BoundingBox(tableObj.box.l, tableObj.box.t, tableObj.box.w, tableObj.box.h), + ); + newTable.id = tableObj.id; + newTable.properties = propertiesFromJson(tableObj.properties); + return newTable; +} + +function imageFromJson(imageObj: JsonElement): Image { + const newImg: Image = new Image( + new BoundingBox(imageObj.box.l, imageObj.box.t, imageObj.box.w, imageObj.box.h), + imageObj.url, + ); + newImg.id = imageObj.id; + newImg.properties = propertiesFromJson(imageObj.properties); + return newImg; +} + +function listFromJson(listObj: JsonElement, fonts: Font[]): List { + let content: Paragraph[] = []; + + if (Array.isArray(listObj.content)) { + content = listObj.content + .map(e => textFromJson(e, fonts)) + .filter(e => e instanceof Paragraph) as Paragraph[]; + } + + const newList: List = new List( + new BoundingBox(listObj.box.l, listObj.box.t, listObj.box.w, listObj.box.h), + content, + listObj.isOrdered, + ); + newList.id = listObj.id; + newList.properties = propertiesFromJson(listObj.properties); + return newList; +} + +function drawingFromJson(drawingObj: JsonElement): Drawing | void { + let svgShapeDS = []; + + if (Array.isArray(drawingObj.content)) { + svgShapeDS = drawingObj.content.map(shape => svgShapeFromJson(shape)); + } + if (typeof svgShapeDS !== 'undefined') { + const newDrawing: Drawing = new Drawing( + new BoundingBox(drawingObj.box.l, drawingObj.box.t, drawingObj.box.w, drawingObj.box.h), + ); + newDrawing.id = drawingObj.id; + newDrawing.properties = propertiesFromJson(drawingObj.properties); + return newDrawing; + } +} + +function svgShapeFromJson(shapeObj: JsonElement): SvgShape { + const newLine: SvgLine = new SvgLine( + new BoundingBox(shapeObj.box.l, shapeObj.box.l, shapeObj.box.l, shapeObj.box.l), + shapeObj.thickness, + shapeObj.fromX, + shapeObj.fromY, + shapeObj.toX, + shapeObj.toY, + ); + newLine.id = shapeObj.id; + newLine.properties = propertiesFromJson(shapeObj.properties); + return newLine; +} + +function barcodeFromJson(barcodeObj: JsonElement): Barcode { + const newBarcode = new Barcode( + new BoundingBox(barcodeObj.box.l, barcodeObj.box.t, barcodeObj.box.w, barcodeObj.box.h), + barcodeObj.codeType, + barcodeObj.codeValue, + ); + newBarcode.id = barcodeObj.id; + newBarcode.properties = propertiesFromJson(barcodeObj.properties); + return newBarcode; +} + +function textFromJson(textObj: JsonElement, fonts: Font[]): Text { + let linesDS: Line[] = []; + + if (textObj.type === 'paragraph' || textObj.type === 'heading') { + if (Array.isArray(textObj.content)) { + linesDS = textObj.content.map(contentObj => { + const obj: Text = textFromJson(contentObj, fonts); + if (obj instanceof Line) { + return obj; + } else { + throw new Error('Illegal Json: paragraphs should only contain lines.'); + } + }); + } + } + + if (textObj.type === 'paragraph') { + const newParagraph = new Paragraph( + new BoundingBox(textObj.box.l, textObj.box.t, textObj.box.w, textObj.box.h), + linesDS, + ); + + newParagraph.id = textObj.id; + newParagraph.properties = propertiesFromJson(textObj.properties); + return newParagraph; + } else if (textObj.type === 'heading') { + const newHeading: Heading = new Heading( + new BoundingBox(textObj.box.l, textObj.box.t, textObj.box.w, textObj.box.h), + linesDS, + textObj.level, + ); + newHeading.id = textObj.id; + newHeading.properties = propertiesFromJson(textObj.properties); + return newHeading; + } else if (textObj.type === 'line') { + let wordsDS: Word[] = []; + if (Array.isArray(textObj.content)) { + wordsDS = textObj.content.map(contentObj => { + const obj: Text = textFromJson(contentObj, fonts); + if (obj instanceof Word) { + return obj; + } else { + throw new Error('Illegal Json: lines should only contain words.'); + } + }); + } + const newLine: Line = new Line( + new BoundingBox(textObj.box.l, textObj.box.t, textObj.box.w, textObj.box.h), + wordsDS, + ); + newLine.id = textObj.id; + newLine.properties = propertiesFromJson(textObj.properties); + return newLine; + } else if (textObj.type === 'word') { + if (typeof textObj.content === 'object') { + const charsDS: Character[] = textObj.content.map(contentObj => { + const obj: Text = textFromJson(contentObj, fonts); + if (obj instanceof Character) { + return obj; + } else { + throw new Error('Illegal Json: words should only contain characters.'); + } + }); + const newWord: Word = new Word( + new BoundingBox(textObj.box.l, textObj.box.t, textObj.box.w, textObj.box.h), + charsDS, + fonts[textObj.font], + ); + newWord.id = textObj.id; + newWord.properties = propertiesFromJson(textObj.properties); + return newWord; + } else { + const newWord: Word = new Word( + new BoundingBox(textObj.box.l, textObj.box.t, textObj.box.w, textObj.box.h), + textObj.content, + fonts[textObj.font], + ); + newWord.id = textObj.id; + newWord.properties = propertiesFromJson(textObj.properties); + return newWord; + } + } else { + logger.error('[JsonExtractor] Cannot extract from object of type', textObj.type); + throw new Error(`Illegal Json: Unknown text block ${textObj.type}`); + } +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..d3cef7e8 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,3 @@ +sonar.projectKey=axa-group/Parsr +sonar.projectName=axa-group/Parsr +#sonar.javascript.lcov.reportPaths=./server/.coverage/lcov-report diff --git a/test/assets/html.pdf b/test/assets/html.pdf new file mode 100644 index 0000000000000000000000000000000000000000..39fb3230ab0c0c744c5680a76b00a3eecf45943c GIT binary patch literal 21985 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 literal 0 HcmV?d00001 diff --git a/test/assets/line-merge-2.pdf b/test/assets/line-merge-2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..ae879fc109893611226de16f933e5a55c823ec58 GIT binary patch literal 7602 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 CSe*p` literal 0 HcmV?d00001 diff --git a/test/assets/line-merge-2.pdf.json b/test/assets/line-merge-2.pdf.json new file mode 100644 index 00000000..e58f5697 --- /dev/null +++ b/test/assets/line-merge-2.pdf.json @@ -0,0 +1,19 @@ +[ + { + "number": 1, + "pages": 1, + "height": 1262, + "width": 892, + "fonts": [{ "fontspec": "0", "size": "21", "family": "Times", "color": "#161413" }], + "text": [ + { "top": 52, "left": 261, "width": 51, "height": 31, "font": 0, "data": "Lorem " }, + { "top": 52, "left": 322, "width": 52, "height": 31, "font": 0, "data": "ipsum, " }, + { "top": 52, "left": 385, "width": 56, "height": 31, "font": 0, "data": "sagittis " }, + { "top": 52, "left": 451, "width": 12, "height": 31, "font": 0, "data": "a, " }, + { "top": 52, "left": 474, "width": 46, "height": 31, "font": 0, "data": "dolor. " }, + { "top": 52, "left": 530, "width": 56, "height": 31, "font": 0, "data": "Nullam " }, + { "top": 52, "left": 597, "width": 45, "height": 31, "font": 0, "data": "turpis " }, + { "top": 52, "left": 652, "width": 46, "height": 31, "font": 0, "data": "lacus." } + ] + } +] diff --git a/test/assets/line-merge.pdf b/test/assets/line-merge.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a26cdeba57cc5668ec8fee9fe9e4f01bfbbf998e GIT binary patch literal 18847 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 HD|P!1OE0a7 literal 0 HcmV?d00001 diff --git a/test/assets/line-merge.pdf.json b/test/assets/line-merge.pdf.json new file mode 100644 index 00000000..ecc45af8 --- /dev/null +++ b/test/assets/line-merge.pdf.json @@ -0,0 +1,23 @@ +[ + { + "number": 1, + "pages": 1, + "height": 1264, + "width": 894, + "fonts": [{ "fontspec": "0", "size": "14", "family": "ArialMT", "color": "#000000" }], + "text": [ + { "top": 109, "left": 108, "width": 22, "height": 18, "font": 0, "data": "I’m" }, + { "top": 109, "left": 130, "width": 5, "height": 18, "font": 0, "data": " " }, + { "top": 109, "left": 135, "width": 9, "height": 18, "font": 0, "data": "a" }, + { "top": 109, "left": 144, "width": 5, "height": 18, "font": 0, "data": " " }, + { "top": 109, "left": 148, "width": 67, "height": 18, "font": 0, "data": "sentence" }, + { "top": 109, "left": 215, "width": 5, "height": 18, "font": 0, "data": " " }, + { "top": 109, "left": 220, "width": 29, "height": 18, "font": 0, "data": "with" }, + { "top": 109, "left": 249, "width": 5, "height": 18, "font": 0, "data": " " }, + { "top": 109, "left": 254, "width": 57, "height": 18, "font": 0, "data": "multiple" }, + { "top": 109, "left": 311, "width": 5, "height": 18, "font": 0, "data": " " }, + { "top": 109, "left": 315, "width": 49, "height": 18, "font": 0, "data": "words." }, + { "top": 109, "left": 364, "width": 5, "height": 18, "font": 0, "data": " " } + ] + } +] diff --git a/test/assets/lists.pdf b/test/assets/lists.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7b5033165627ee390b770a7b196d180674ae263b GIT binary patch literal 20736 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 Qfm>1G7Po{1)KHuIA4-f|#Q*>R literal 0 HcmV?d00001 diff --git a/test/assets/number-correction-1.pdf b/test/assets/number-correction-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..28dc8a1ddcf4ab49338f1cbd926030d436fc31c6 GIT binary patch literal 15866 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 literal 0 HcmV?d00001 diff --git a/test/assets/number-correction-1.pdf.json b/test/assets/number-correction-1.pdf.json new file mode 100644 index 00000000..6f9c28a6 --- /dev/null +++ b/test/assets/number-correction-1.pdf.json @@ -0,0 +1,322 @@ +{ + "metadata": [ + { + "id": 23, + "metadata": { "order": 0, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 12, "metadata": { "order": 0 } }, + { "id": 1, "metadata": { "order": 0 } }, + { + "id": 24, + "metadata": { "order": 1, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 13, "metadata": { "order": 1 } }, + { "id": 2, "metadata": { "order": 1 } }, + { + "id": 25, + "metadata": { "order": 2, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 14, "metadata": { "order": 2 } }, + { "id": 3, "metadata": { "order": 2 } }, + { + "id": 26, + "metadata": { "order": 3, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 15, "metadata": { "order": 3 } }, + { "id": 4, "metadata": { "order": 3 } }, + { + "id": 27, + "metadata": { "order": 4, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 16, "metadata": { "order": 4 } }, + { "id": 5, "metadata": { "order": 4 } }, + { + "id": 28, + "metadata": { "order": 5, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 17, "metadata": { "order": 5 } }, + { "id": 6, "metadata": { "order": 5 } }, + { + "id": 29, + "metadata": { "order": 6, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 18, "metadata": { "order": 6 } }, + { "id": 7, "metadata": { "order": 6 } }, + { + "id": 30, + "metadata": { "order": 7, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 19, "metadata": { "order": 7 } }, + { "id": 8, "metadata": { "order": 7 } }, + { + "id": 31, + "metadata": { "order": 8, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 20, "metadata": { "order": 8 } }, + { "id": 9, "metadata": { "order": 8 } }, + { + "id": 32, + "metadata": { "order": 9, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 21, "metadata": { "order": 9 } }, + { "id": 10, "metadata": { "order": 9 } }, + { + "id": 33, + "metadata": { "order": 10, "title-scores": { "size": 1, "weight": 0, "color": 0, "name": 0 } } + }, + { "id": 22, "metadata": { "order": 10 } }, + { "id": 11, "metadata": { "order": 10 } } + ], + "pages": [ + { + "box": { "l": 0, "t": 0, "w": 594.67, "h": 841.33 }, + "pageNumber": 1, + "elements": [ + { + "id": 23, + "type": "paragraph", + "box": { "l": 54, "t": 63, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 12, + "type": "line", + "box": { "l": 54, "t": 63, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 1, + "type": "word", + "box": { "l": 54, "t": 63, "w": 21.33, "h": 14 }, + "content": "ooo", + "font": 1 + } + ] + } + ] + }, + { + "id": 24, + "type": "paragraph", + "box": { "l": 54, "t": 93, "w": 32.67, "h": 14 }, + "content": [ + { + "id": 13, + "type": "line", + "box": { "l": 54, "t": 93, "w": 32.67, "h": 14 }, + "content": [ + { + "id": 2, + "type": "word", + "box": { "l": 54, "t": 93, "w": 32.67, "h": 14 }, + "content": "OOO", + "font": 2 + } + ] + } + ] + }, + { + "id": 25, + "type": "paragraph", + "box": { "l": 54, "t": 122, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 14, + "type": "line", + "box": { "l": 54, "t": 122, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 3, + "type": "word", + "box": { "l": 54, "t": 122, "w": 21.33, "h": 14 }, + "content": "o0o", + "font": 3 + } + ] + } + ] + }, + { + "id": 26, + "type": "paragraph", + "box": { "l": 54, "t": 151, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 15, + "type": "line", + "box": { "l": 54, "t": 151, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 4, + "type": "word", + "box": { "l": 54, "t": 151, "w": 21.33, "h": 14 }, + "content": "o00", + "font": 4 + } + ] + } + ] + }, + { + "id": 27, + "type": "paragraph", + "box": { "l": 54, "t": 180, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 16, + "type": "line", + "box": { "l": 54, "t": 180, "w": 21.33, "h": 14 }, + "content": [ + { + "id": 5, + "type": "word", + "box": { "l": 54, "t": 180, "w": 21.33, "h": 14 }, + "content": "0oo", + "font": 5 + } + ] + } + ] + }, + { + "id": 28, + "type": "paragraph", + "box": { "l": 54, "t": 209, "w": 36, "h": 14 }, + "content": [ + { + "id": 17, + "type": "line", + "box": { "l": 54, "t": 209, "w": 36, "h": 14 }, + "content": [ + { + "id": 6, + "type": "word", + "box": { "l": 54, "t": 209, "w": 36, "h": 14 }, + "content": "O.OO", + "font": 6 + } + ] + } + ] + }, + { + "id": 29, + "type": "paragraph", + "box": { "l": 54, "t": 238, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 18, + "type": "line", + "box": { "l": 54, "t": 238, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 7, + "type": "word", + "box": { "l": 54, "t": 238, "w": 24.67, "h": 14 }, + "content": "o.oo", + "font": 7 + } + ] + } + ] + }, + { + "id": 30, + "type": "paragraph", + "box": { "l": 54, "t": 267, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 19, + "type": "line", + "box": { "l": 54, "t": 267, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 8, + "type": "word", + "box": { "l": 54, "t": 267, "w": 24.67, "h": 14 }, + "content": "0.oo", + "font": 8 + } + ] + } + ] + }, + { + "id": 31, + "type": "paragraph", + "box": { "l": 54, "t": 296, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 20, + "type": "line", + "box": { "l": 54, "t": 296, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 9, + "type": "word", + "box": { "l": 54, "t": 296, "w": 24.67, "h": 14 }, + "content": "o.0o", + "font": 9 + } + ] + } + ] + }, + { + "id": 32, + "type": "paragraph", + "box": { "l": 54, "t": 325, "w": 28.67, "h": 14 }, + "content": [ + { + "id": 21, + "type": "line", + "box": { "l": 54, "t": 325, "w": 28.67, "h": 14 }, + "content": [ + { + "id": 10, + "type": "word", + "box": { "l": 54, "t": 325, "w": 28.67, "h": 14 }, + "content": "o.oO", + "font": 10 + } + ] + } + ] + }, + { + "id": 33, + "type": "paragraph", + "box": { "l": 54, "t": 354, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 22, + "type": "line", + "box": { "l": 54, "t": 354, "w": 24.67, "h": 14 }, + "content": [ + { + "id": 11, + "type": "word", + "box": { "l": 54, "t": 354, "w": 24.67, "h": 14 }, + "content": "o,0o", + "font": 11 + } + ] + } + ] + } + ] + } + ], + "fonts": [ + { "id": 1, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 2, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 3, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 4, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 5, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 6, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 7, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 8, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 9, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 10, "name": "KYVLGE+Times-Bold", "size": "19" }, + { "id": 11, "name": "KYVLGE+Times-Bold", "size": "19" } + ] +} diff --git a/test/assets/page-number.pdf b/test/assets/page-number.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3a28d6503eac33e6bfd69ce15d886fc798c5e932 GIT binary patch literal 15350 zcmch;1yE#Lwk=BG?pA2xP`JChTjB2R?iB9s?i3D%ySuv<E`>XZ^Es#c-spaPU;Gz8 z;{7ela;~-J9CKvE&fGD`6hy>m8R=MI$-03B@32e&27sNRB`gmQfL_MJ*2KWk!qdbU z!1&n%FtReUG63i$09pVx4t4+|6DvRmKratq1JH|pe#pSi3ZR$!%k=x-Jr)2TAFPS3 z@n6dU{{KE;ng1n_?;iFh0D5&L6VuNyOl-}Z%>jP}B4%OjY~uJ?TN^l=h?p4J8Gl;+ zqgF6*v~dFb1%j}hwTq3d6MzLkui$86Y+>YV=lB`=XKVm^wNE4%0ZgCwCk~Rf#wPBc z1E1Z$agnq&wF59Re!8IqpjWc9bN*c7@3!(Mq5wwrzt?m9CvN{MJoJ(x04*LiBNHP7 zW@ZL<CJs{r6IM23R(56vRs%*8P8JqpV-p6x|91<WzmnkO>}X<O1M8k;WN5r?rf;av z0YjRW5}+<75KxCXz=edG2!bm16~O2QNj`)|0DyrMvjzvbnTBBaD}Dc*{C^{nuzw-) zUu;t_Ff(!b3)#Ox`bWxy4V(?E?acm#NUZ-d<A2PbGJyG$kN?av3&+2QudlCfqOY$x z4FZ0+{&gE<%oNfPGl)`x7MqzN%WNA=YEshar~bV@2uR@;MAUx<&-O2y3)|T`o7g%# z0oecHu<R!{4210dVgSSE$;!zF;9z0?ix{6=`ZWCS$dpW+>|7j;K4blhUZU>K;>ymS z{QJ8n&H!Nii?~XE(@X4gi@%4(K8OE7p1)Y9;Am&0Z1TCY)@NHp?DOkp;_m#(Kbubg zh5lI!|Ff3-bl|T!|A6x^i2nu4zdZjh^#4aJ|A`_K1LwcNn5{4l3(5%J0i<@sY$CUr zZvYU0-&dl)g6IJ%x1T@soA^?As}YipWy}&%KpJu8Xd#e>mw05R?PH`kGBv{TxikCW zFUmCxqUa(kx~?TB!F|q~uONRfPBcR=n@uL+`HwvDUEHu22CBej!>86>K0&TIsZ4kW z3iOIiwV@_O=I<0k7qe!}BUAD8f}*^kj!jmMgEJ4kI@Eu6tEEf|hUlszQX$eXJSRJ* zu&)eIzeMFEa8G9uPh!9c>SOV8OaT?+hyD6)Sb7htk7jHC9~|fSKf{F)z|6qF!SUZ@ zV`SoBWcxRu&V4+*lSJqIT=;pJIY&w3xQ%m+nZQViU|^7YB*^3J!?cM2f^jnB!c1|I z6G8sOK^Wk~p;(me_F)nJOyD#AQ0~+O?mG~m->O^a0;+c{g$~tzPcygbyKMOYfzvNU zd%&`ji_f$C<ir;h^#_PQ|3V<RMH8%*%W+qo)fXXth(BFJY|~^lHShWaJ#o+=Hfxn9 za}jO2@~@yXrSJlKbhLU5(l16e$o>J{Mjk5Zzcn3=uLijt29A)y#%qa-h~1%X_(@R5 zzG>gaTDxe1i~#%zGRG^dRl{O7;q|`;AJ||lJuP-!LT&_}a}bUsFTuV<7O}wTuX+3b z={dpix(CWny}<%7*5By9EN6Fp0JUuS>kUj5mgxCr`FgsWe1B8N&%Z6RbzQ$)NEbg7 z@huz8VjYAd_mtY2pK@SVhi<twoBj#?d2geavDqOuk4w0!4~C#HBm4~>O#cf<4fyv8 z*Zjcs?ULLc*$J5Zu-Ud8h0=6T<x$5pF}EHe#4-x?6}q2-tN7|uf}U|e(WlWc<uIB7 zbde-Yc1#0NnO&14-kz6T>_u-p!EA=fVGxxJsvK#KEzikQ2Dod68?DvlS{-ieZ?zo= zFZhNa>DrJlmCG0YFLm#N*3AaL`z1;ZJ>YKU>+~|lUQSenNlT$=mw>$v7YfVk(w-=g zOr=NC+R{WZ9{9ViSw^69C}E70^Z3UM1CBRbm4F(rCPQwe7K9_csZ*5JZ}`D`H2n|O zXQ}=N>v6F{ybTh8$PQtf&RFmzyxW{bWi9WxNiHP1p-f$eO|1Fv<#MUQvko%i+gZ~j zQO>nTxJ6y~&ZiOUZTbyKU+yK200k&$$J`3N)ldNsYk5zQq=->6wxx+-RvIu-{Y-+q z6c!J?V!C2c81|lYbx;QNcg*m8O_`0S4)|o|m%d?<aZ(?MK8@t+QP;_ld-Q@lQARl! z*9xEz9DcBK@DI^p`RgbZ_SLt#+z#vd9x%_Wgh!S;+>kHwLQx{!3nXK$d>nq?nmK{o z>2qAzCewKn&WM#rSUrumY5P9zet6?WLpw{Zuv&_ytZVlyN+Hk(T<%1>G7wFjA$uak z4PiN9=mYFwRbiZAL}6ygoes7=9%Lvr*vvd^qTc$}=DOmz1-8Vd$*epyuVrWHv?uD` zIUMJ7X@$fwv7<n^Sd9gfSU2mgQuhh>vam}|o{P~~?`)ouHOdR#2i~ZCt}x!Px`Sfx zt^6r({>sus1Bkh3?BV9?w1a04s%E%YRs*yRv~QCqwM#5@;&D<SVysJcw@Nj%Yvy0A z`xzq}2Gf+;`Y8_s6z6mlCg=pJ7oe0_EYw3!1vS{;2yT23g(06Ec~aP&^MOu(V&Ydb z`dg;5&8Lm7kk_2)xW)EX84G65?z~IC+?1G7=#V~+o2$k3OW~H{;jh1!-haUS8P3Q~ zV|Md=FX24A-uApW^b!K{N(ztYM%+h-d5RRsqqc6DAzIAILUv+Kvx#8T_aH|@Fs&Ng z$x|MBY{04WYuu9IC8+jGxlY`EEerMZgtuJ|4oI#Z&I)mwjoo2G<l~z=L3CK4I&2$C zNcG%nq(0!(i>3loqP^&LQ^`_vK)Hq+nk5lQCrLaqbKujD5!<L9s5k;PJ$j=Nh<b*y z^R*8gXVG1MonfVGS8{05_XS=kFBlcOz47qh;0-&GPCM~TyC1aAA->`MLHs06=++O5 zldc#w2mF)A<TfB9Qen5<m3spTPplQtqe+@5=X}k1pI_9RCw8yxs5!e#nYZf1V{O<g z<=g%CC9gQ^SB3n;dK==cS8!m2lXJw(v|2kZrd7u5Wg1v;SLPyb*n>#tQ7v)9lbCw& zX;@Oqp6`I?fU_;y4xB%<FWnWhqGqV;q1miui!4_R5Mq-ZEMU+h9)UgY!Y+b~N15}$ zB?DK#;=xP8ERk`UAO!V-x(l%y3D_x53;rBrn8N}2wt_~92J1`Ntg+Gq8XMWN?Q^u< z32l>K{{zv;6>se7x+#ng6I$`SPIg7;ZnzyfmfjW)PZ<~XX_xPs*LqGS1lJzPW_Gb3 z^W1pDDZdtf*d6^v_OMOAo6<qICJ@)buc~LG0n`aG>@oMyBE@yr^Uz3FT;krmFdI3) zOdNL##cJ9_Dn@4t7~23g!6Vji%7AqHGQef7FBw;w4OUTS+X#6}I^_kmL_7GF8RF$e z_Q3wr1K0i{S$+D&^tK(oo8;=PpkBburn3dgf9A)Qvc2yy_X@EN6g5MPllM|Seu|(T z4?XmdE=@r$sG@F>VKS&<sTyT?;+=iP&uM?8KZv7*dnvj3P%EhRwN!C*_kj|a@II?A zEoy#&P(^^W(U=~vN*L8^rc=aDGWKde!cAI_H^sD`(_<+0F2a&JnzL@uZ>uu)QeF%X ze4zYna6Zv*ELIG1sI?#R9M!NltTKru+na6pPg^|gEm>(sxkr$QY_QJ}zW4y=`+~?R zXZW>qt~;ue@h7?Gf@&L2PnV7WU&Cx~?XzK%6)^ux_#8CoL?JDTxy4$vx^!Q}{^Gh? zf9qlJTR;?G3qbJwnluX48Oe}1=NfE8NCXxaWEzkH$c?&#7_cpla%=u#k5>mj?2B@& z`>xv~7}_f!S9f7B@D|qGVT0d$)TZPkZeEb@<iWq-6qn1J%F#Afm<_e-4Yt~7E)PDm zn~k?N$RNv(^tKFlYUD6MXOcyceIB~%S2CUZ1234xKo}w=#_H}F%wnbc2O9p*i84>N z*P6Q*dH;8vs5GLp7qFmm#)W8a_-lz$KsNDWpEn{6aL_S-IR|{p7ow@3x1ej%Pl|U4 zwo9q6QRlQhrTzJSdd!)gXANbO!#pyy-GB;eT+Bmc`PX!2ZIbZIx28@{k96L5iZsaZ zIk9}SFzi#MrI=x6G(j|lWy)DI3~{X|$_o?lRE#$;1J=-$cIY-FJUp9oFa|#ot`!$R zGxZeJTIc)qb8&^A0zaG=79+xRgc}jUF=-K}?i%KnW!E3{TeM_%JL+5Q8(FOM2j1B( z_L!Uc!!e_GJ<yA6lCWiznzxUfNVBgwBzHXWSv_77Y?~ypJ8%19EePAFhY2a)2aM7} zVdH!QT--gI9|XaP38P$!B?v*y-yxoGD)~!#gf&>KRWFm{)qxFb%i{QU33J^~2>x4) z1-@HVcXpdU(xP8u!?fZ*K#wCOF2$d^4Z4dZ^pw`<CkJkL2AtI23C0h?%0{-rS<o$* z3b={EUuurFf$`&;!XKx1W0!;<bc5WH2=^>kmmWkqEBE-jTP3=F?=a`j)jRhexwD8{ z%zuP1xVO3I)!Z+g+`NHLg>HECqJB-T;F)KwL2hFpzOjdH8oqv*K=I@VAiG2CpvM<a zcdBmg+ZXD;i#b{eHFH*>+|gIw+*62M36wAtTu|@Zx}dDB@A_R}PeA>rW<djF8sf-N zO-J@sp^bg;>lvYEE}ukSA{5C4Z?eJ$3ClQDNv$F`4G`SvlKS*B0#`z>Fy6k0UuLUj zXWWxtCdV_nEqs?C%?leL&yj7qTw|JqxYgPvyxJdp&)Lq=)_$)wHx4&+57I@x$s#5Q zAE<NcvW-(YW@Hf|#n<oWJ63Cx(Wf+z#*Vk|vN-(S>)YMkn{c3VfN)?RtcqMGkl$;( z7Q809&c9Z?4teCemb@<9t-5w4VLgd<AZJii_LsCfF^FIoOhc6!EqO1YQEn~UY*=XM zYZ!BmwTQGxEt_?oxyw4nnAa*PESZ+^V>L^sJv2BZuAk%?=Q*l8v>sR`!uk{JJK3ON zOR$o)$kV`C%GvDU?PPFUHR;AuWA)I0t~b_or)(8jI=Hv9l^-(M=a@NadxQ9dPYcA+ zyikT&qiIH`Z5HzkLRpHIHD<5lUiEB>w=t2gBY&Xcu5hr*S?nln&C{flvzo=!nRUc6 zJT+n;|KjMzshu+G7;fI`>``mkx@z7kKI6#ErD3bI5$)jCq(O&Gi}jVTLbIB(OGI7n z9V;bs#<Z1NHdB^+jm8a}7D%OOE0K9{cDQO5UW?b-lL4biSLcDOIW@=A;URLHbeNfm zo3YH&+_2$nL8)1tqcwHQ3VFySkK`G|M_=qIH`DAO4sIZ_DSI-4r!@cT>o_7oeU)TY zc$JT)7fCy^ZHR{gqKQ2(yv<X=fylTQtzBc5?Ni<XWvcb+jHBOpbqeicYpIFD_*_bN z1BWIPNLS96t3{ZD_UF=6q9q@Xz2o9izZb`sV=fxrz@84zww=2wy``KCAIsk3Ks}0F zT_?+zvU`f<+zrngME}It>#ov1`6=G;^e5vr$??&sL>LOOtP$>b7sIRY9~4Y-McIFx zh~Gu-!U-rwt_frO{9ezl?PB}}z3~0cAM^L<@rE^Gx;<ZK_u6BR@CjXcj@<jMANL%F zjS3OP-w5uD-yu4VJM+9`fN>Xszros~+Ys7bT<lJA7Hnp1`fg9T&lM25vZdI%+(vt* z_)GKrRA$P&1z~Sficuc}=`HX~GpL&@Kc79>3mj;2F^Fg{R>o|FCX}vfx>mpuQ)*I% z-r@;#JTo*d+4y#l$ll&f#_zKuP<cS54bq*_Dnq;b&TAnr{6n{4RzlQ*AX!dr?2)wn z2@O~`ps2UWIKCY17_5Bx@Xy^gTY+ugMsP>S2{Nqh%~k*v??J5T&%C1RfUxW5zM`E7 z0PawBK*<YIy@I&)ieG*44y${{_eP>NVB^q7{H}ke+GqMRpj82ZbvsDY-aux#S0gQU zwiCqmhZf^h?ho2pY)nC3njhW0TRR{c@)smY5<w^kEW&WDxz+&OQ80TL&G3{zVMy*H zf%`-^468w{=5W_!z5}`7aaH>$EzsQwHv5V<*dAHd6!LNS#uTffIi&IiQ;vzeMe%3B zetnE{0vCXn5#JjYj}Vr4fw^cr0F)&1Y@$9YXz7mhT=pGcI^jry5{9H=<aQszF$|BK zJ|R_tBi#S@HhDa0M2Czn(JX1m9ckUZ$_>sdWOu*X6)Pih`arz<4VOn?hh$7#^T>J{ z^A?#{f`H6`OiTiB<Ygc1hUf{}4!TpqZZ4Fx@9Sd-USRAFx;JEQzXJzMZV%QzPpg3M zK>Z2D4blyBtAsDv2WfZwmL#eLIqg2G1=*;3%onmGrEsS5e9iJgzJ=81_^yRq$QgmP zXh=m~gc70#?sGvUb4mn~GScXfd1xHTLUWerd|nG0R)zHH0tc3yJCfrC2_|z;Rz>D> zQLlMEmU!}64Ci988kmx!VzN1#IVOwN1|?0}s^p9KB}v?((5rp#@18<yRJ>H@DGw4a z#b9%{bCGiubD4A8Eoxh-F_O8GdJ=k4TM}E6M@6@Dv3KInXm3z&>F+}Hb9%@8$98va zYcg7-{G)oXJ@dia1x<D0t43!IP1(G?uC>|LSYD{|$-gHiA8N&T(t&kvXBH3eIm}Qi zf;^?UM$`3JD@3koog=!2^z~{h6=$puTwZ~lqcOG3E9+<Y4|LpPn?{%QNGqXdB(70D z6MVhOC(I9=+*5M3^ed9C5!t4CziL*N&){D|-jaRBr+!_oY^?y#F3<2@eq@_rt;C*D zJRm(VzXW^BPSr-9sXfR);J#$D_7j*FAr$eSg_iD^cyq78y=-GV!gWB8Gh#)j-W0>k ziCFw%5p5I9_$HeGyNz{2%R7PD>s0$?YG=wD{epb8Hf2A~W1o(P{({J5Mg%j#X&;I$ zb+MLgf(k=wz6iJYcn-cPY4I0VQ`DqUb3KGLr&h1(FGyD?t--^3uT{&Ykg7qnCprgm z{vLt>?#X`l`qo?<{pMe?*39bx6!#=^AQpLdNa5WimR~yfDap|Z8>H8I#lkHbxCr(5 zKm2=a1)sO$E_j9d`$&ij3yeI6aQ0i=jRHnDD%^4hiuSvx$}HaIn4gib(>Tqq;;epE z<7e26er4lfw;LEyEUdwGdRuYWqmY@%{m$SqhM){r`kTXNNR~OWQo)+KZu%Ro(?pK4 zuA+ttZ}ZS`dA1t6U+-iyO^3_u?lYNRtIhGR_0$^mRr}%cMXRMvkLBOMjdt5NAE4dJ zV^1IwZ->QgN>!}2*InIkGk={?X~XGgscSos<>Ghq$FW%kBA)j{>yaLZ^W$KuuJ6I( zvaa9WB#B+;+f=*wRv?i7@v6{>(C_BhESHe~dT>-(?K0+ncYSRBo=r$J8YS?wSz;B2 zL>j}J;V0?T)3lV@+AkX5>-0*Og&BI}SVdlECg=LbM9f3eAP$Vv_fcr>0rBJ4Bg|Iq zCzS$lPIDAUxMoIfOjzTwmebSYR;e4DD=6-#)LDv{eQnkz9oU~rz)Z?o6rC%O&|D)8 z*PRNe>>72<QFQH2v`WexX$V)2*UPq|N&0;R&aZmq2Fr*1)WUHFI1)`d7_(I(J!%!7 zptc&J9vj?wczi)%M{It%mkr_deII!yIYMMty(HQZ4%at#-;Co$_Xbs@a!X=lpT@VC zIKXKkQ_g7|;5FD?=-Bwv($d#8YRj}?O5+;nkj70Z8P;RZ7_G&<QH3@?g4l)ABD|UC z-D04UNDvX304fzlwsfrgJE1Xd8YL^}_OPwrjt^~Ccc51|L|$jJj#blgB+`DQV`ZjR z+tn_IqtVC;&kqxG7kec&LkyiYs;5jXA7xl3!GW!<(!M;Crk~2B3$^Q;iI1xhK@C*~ zZM#o&f0PMAedG+QB7*IR9`Z6ah6(33CxW7fqj2wcqbzRjHFfjJ=+${JoExmO=!TK_ zY^`VV%ULMukl*vP5+<Oa+)NKmY`riJQ_-V#^)x#;U&W0CT1+ydGGthHGa#=+eUMpJ zAoH<(F;LGA=mcXqHoE#FaqfiOc`q-Q(yn4wuHX(Ii1%8%-Ky)#Sv3>iv9@Vq6PpzJ zUFk37q#P;8Z;)pZ#B&Q`ghf3Bw)Q<_7tAY{#wBLF8NS$9(c&dU9L7fOA$Apxf)1+N zFUx0@Cz(*j*n&%2D&=QlUZ?bLrp%0X6mPE2Ep?8B#dpYe+y}S$OxS!t9QU^!B_lW_ z*7BP-QJ));e;t;|VpEy2qExcsF3D4tv>;qqDsjBH=r5I;{eEEHK&r_|S=tP9+qVG1 z&b;HNiLyW@%S9L^r6|0QV(T+Yd7$wJ3kzuHYI&o*-3e}SSyFyXkg(9|EnDhHaSw_^ z`aLcEI4!^e0l_zWjbR}PM!W)?gaEc=Xte(~!&kQwm=CKPJc0p5?DM9)(GaIEQT)YH zw63U8s!u|%yR)n89^m9{At5NAKZJx80~<$`^{kWbheE*a$exviU3BBw?K60wF2h`7 z)m6fpzL5w&A7T?pz=qyJBME_m@hPM1p)jLZp=2U!5)E6GBS)fyB4dd}io)7;aQDvz z?IQ|4Owq|>iB>3O@d={?Hx+)))S+QyaBHIA7KGiO)_gC8S2nLP4h~71JLSBGli|-o z3sD0Ck<rVqCI3((i$G)R*Q3MQf+sgvMvKC(X6nmrIZGJ&FMkQ&Zd};rq=_Hu*}lW$ zKgmBL0*AkY@|3g4KYDP5liNyi!heHxB|#)2ea7(ZD$?sS^u%!!K<2CebTt%N9OVc_ z(-Vik^vWqAN2H7%fDs>^7QPb15yTNYGfRI}R!Z^ciz+{5MncK^L!Os6W}GW#8t}~N zm^(kM*ocJ73{^^qETdFDhAs0Aji8E&$lzG^E^uh;89U?8veyA=|6vx(GfE2zI>ie# z)CaWj0JsQB0ZzW?h0#7=i043d*d#Q&P^1sv5FepYE=gE33ZXDgUqG7wRsJRGE{p7) zd7(!d5k*v&v|gw^%L(@#g5Xw34a_s9kID>t{17p(ke*c(t3V8Y6WGHn$s&pDpeurp zgPl-VZz7ruQoaZLql~CA$q<Pt@e*;v9dfiC4P($YV7d>5P+(N-QEp~fzXD0!6qn|3 z2iu6rFD+C%5|58<G;?CXP`CR_<g0<M6T9PQ*#^t`&al-1{#m9@3&-$Jkk!ZUA>B5l zk6ed(cz<zxG%rBnPn;Q#<Mc(0cM34a*DOP<`W~uU4kmW`2<2;}-3*0;hBj-6ODpMv zif|7l`Q}u_m%Cs`cJHdx9kxJEbgxnLPOg+Ia$+RmLsQlq_iQ!_U=ii5J3IZ3XB`f> z7ngWTEZUSjnJn5AIYE=?lwqQkWvO;>4gq)soK5b$z#caW&j%_Db2OOag}C6^z6d|^ zL|la>Km_l*i*hif%s+O9uS#*6NO-DDQ9d)ddVl?W#grw);5;*qEk?fwpx*-#e4G%Q z5Wy465_d<|Oo|a7T#hh9{-pJ4SbErC*e!IG!Vyn^P;&XMZN7SW`JKCN0sS5Y0lx#w z)SNP5q5NEI!qQM&o;nlX5}GtKQB?OqDEJHb-ul{nH{|>6vcT2kb+OeS%|GEWEy~4K zxPGS==EOkuj9k|JpoAAdiorsBuRb|>QD6_%7#LtEbp9P#*o|1$E-F3XcL2`hXh>iY z?&W>MPIDS_$faQh<u5a%99C~)cE&k#ju2MvTsCj?`nfAp%S5Cl9y42F3B)m>JAny6 zf1-HMA5`m=6?650(fJT9BQc!AfiTGSIr~I3=@fu)mq&&HdQ2*=vUM$o!$c{<DkJ;X zU+86DI;Q7CSiTcU8lWkz@-Z4J8C7E2JDQ?V)svnsotB&ookE_>xs0}PWD71UV^y8* zpzRg{j}Otd_m1}V3hxd07NER6Sv=&l5+2$8z0B{kuKbt<{9$zEpA!9Q(vDbng?G5@ z-BO2U7!yO{887<Tu$5`r>E)Q%Q22QSSceP-@R=Zo(BYksq;TeoZui@n6Ng${&}_5X zLgQG2)lW;PGoW*PWe55zQbcbnPQCz0-B(Y{<-F*%bkw0y3~qms{1Og<L?Vdy3Koxl zma+VZ_%i@>t|4T$DEAVd7INb<<KcxHZn@lXTLpW3_a>qTf^<odP-S@v;7Iw7jUV05 z&204bG0GZ8#DS&1hB;z0^22iGs>ikBjque)Y>0_Y1-&WK{Xk2)y@yDQV}5#u<rHh# zATSfsy~qgyCqknOR372zKZs!xm!ULbf=RFS<RCVV30ide3RECMMGGXZV-yM!5sX3( zCsGtLmTH$M|Mu&btZ{LgB=I>DV}#7MMa}1(B>C*teb%3lFtTX{Ycv1kB@oXz9Yg83 z#aKhrr=v$R?qwZ(0vFKwb47KiZ1IAQWzFd~n^ASgQO2>FdCpX34dda(k7ExAWuQ#+ ziW6>4opz#G!y)DcK4-q4X=$r!)8wLG9|mM*57g=O5l*^C@S)5mPr`Ka=+W4|Q*=WV zy3>!aYZztVV^6J6hbwGb>hp_12d3yS1|r<N8WKjYm#p?c!JojWkA6PCj$o*5Tpd6F z`~Bo`W`Cx={B?=#tBJf#gu1h=_MPA_9h*5Lms$sG(pJmtM&$UT5tVnRT`bjB%hujx zvxHkc9|J9duI2+S9T209@Xut|U+1)om-ep_caZ{-?XtW52z1$#$IvVB`l!juWaE}i zuDSH|RIVI9mdl+vwjl2BRyzk!rM0(dk~Zp$U@1!iK<`z3kZYu0-R&`VL9<E2IXaD? z8wGcM)GWiPRgIOo+LwJl4%W%ib!zZ2Ae2$b2t*DKo8v)@pMXaJ$+1H*<Fs*cpSp3C zJKj=gk%Z`Rbx+{fmgE4ZkB95&u6F}@_|kp7-xpYmd=Y%$9wFPGksKFGh_JLJP0afO zz-Zd$Y=YPnT*cK#!3$zQ-FZU9>}NU_K5K*rB_eRt2mRh_>DXC{S<+ACifi0=uK=@H zw8^|!KjhSGhRXvBRm9Gd+Cx~-6O!x3!*9-apXJ}RVuWw)ue@)&^`_seaiVxb^z%pb z9K0A$an6=vAjeN(eC()Tw-JMkpu+VXEmT2FseXqfaUR=gYDx?+<~(?|K0zk`s3M1q z<D9x2Hvs%mT5O@MznD7?zw8x{w;U)77SB2l763KDfTh}uutOSAx+*oL!PH5(F!{Mh z-?}USYum*=7JD?ZK?*HTa#@{)-HR`PU<Qf2uqoDPx;sJdnD~_@Mc0Ugr#}yO?SRhK z#vP-JRFO0}ibC0(P*apJ#j*#=>?|}F)jqHDa(GtWaDhh_?2qCxN>d`Ml~+^FgR^jK z+RhhuXkF4DElTMeqk1F5`&L|8Q$<KKHAP;lXlG#A7-~Lf-+L@qV7e2KnZjg9*%O$4 z#nDi>#Ut$F-w<$0JDGpU7M_+inbSV@V1wsROiTP-(yHSW`e1#!u%xEBW%st2c+rYa zZpcF;c)r}*-|`F09-Xg$n(8saJdFQ1gz#NwYjhv;m4>)tRP~zoj}OPV*FN<PevUwQ zoM{9U_2QiQyN>EBwgrMN<z+F_T;Zcf?0fb*d9d`gQUZ&>SORSEw*|{qXA0j3X}w>R zu1Ov<Q^y(i8L}DY8QB@BkJIzce!V1b=PBZJWV&3duvOHW%gt;}7q{Lp`G16YXs$56 z!8JDTnz225@rrHOKE{FPofk+Df0e2hAD<wtAJt|eq-{;ICXI#tol88OpUg*eq<AEN zL*gu#5B7(Fi-4b9kvlumt)~5$@0syUXHN26DGvP`nG{L!*909$Nw}i>1T9akXQ5}Q zHp<5B;r`dOAA`)y(uO3fGOFVQrdz-4Sm#;oEV^c&EvOrstuh<<cgakD+6LGb*oN3v zU0D1U=M#Nx|5;0zbNpC{=%}eld?5K^fH0{a|2T9*H*_mQ89$^H^}Q{-GOszoheeQC z&kLb5Bq|W@(bA9JC)&0E>zH*;ohd{nVD*(<<teoZYOnh#dr&vT>5Nl*y`AI6+Xh5K zWZDWWFFtH5le|XCf=1F4jxSXX1%DlMIGqrN;^@!b9C%c~(W=7a_pn5Qpwgn&ebp8D zTaecbA5{aqSQ2gqdh|Hw8~M?|=1m7|(e$}xd)$fq+&<2YVd`x@r9LSG1mn2iD_WkA z_JY_FrOclQWOhnWR(Zci`SQ+S-d`q2e@J8y)Fw^Du~|;fPv>FAF!6IxoeQ1E&XpF` zoduBRChZ`$VAzJX=8)A2Fem#6NC^@v_YBAxxoS}s*K-s_GFHe8XFaf7Tm+^;ITqEa zqy30?;vE_^o<PREcH%#T2|?NK;q?VGt#dQTZSrRmEGxn7tpw}Yw44mJ$QjlTEJgpu z>n5x>0X5DG=P+=7g-{uMZme-is}OA$le@ForOO;Zuo!i9SB!AS%oSdJd8XIU6fV-U zb#;p#<)5EnmjA;CG!d{~Kjs8q*v@Xcf}juuoM&zsBeV}uoHH0P#}@~>i%BfDY$`L% z-nI-2yyi_PBkt{Rx_>YIQb8w~o%h-ehkE4)=RD>I>6-T86I{^}@xQ$u!4!ynw+nVY z%Jrc79~2$MDy#}{X?_d#Pf2W-8ks?hJe#wc{k+3Fhw3s0ll&1z`Zic8t+98<Zx-E| z1hG1Ds&zF>PfN!^w-ST|3a@E=Y#$l?LE@F}*B;YD1+rGj3GchzFp@yV(DH%TlvZ7g z>%BrJ<}Fx}pHLG5uHZO;l))29OXM*0ZXsZNhKrF`2M&3^e05^mp($(}Eh@|!{|+UI zsS&~zJ)fJ-$PS~@Us|B%`lye})1rry{)IVVH>y{&%C@KHNBY+W551{S2%t~$iD5Im zj=8(#eti0?+wze6)Gu{5bfJ8TtcE0P0C|#JimAEjqufKYv=fU;m#ZHk@1O9T2iPZ> zTd&wi#Wr6eVd9s;{h+AmvG8<kCSE4~Asi(Xlj!uCY(md?-KZI1Y#3O0q+DcGbX9!v z;6d=;Wm}b{p1hGU*YkpCzYVU*PTPhVKJ29ddRcZg1Me3CsMsbwbGnJG{G};5buGqV zj6o)U(413(2Hqicnru7d=0t}cruU^kJjCW!q|JtoXF;%z$fJ3oR*&eiNBOXrI9$Y* zid!{EIgGEikr&a8Z4qFWEu*iID?_84p)c|y;o!z42qktWIwywQtDNV1_X!cbmpyat zI2@v!NodX9a8g%Qb)K{2;IUau4*DoYlCH!bP{86PK#qTV{<>%H?0C`ts^Ru>^=r7L zw4T?jnAd!i<tF_A6LX8iH|)}-s})9h_xu{pXZev%*1hs&YCp9|VC(8<*i{4LC}F5o z!LhN_{qX?3VcWFx*0JyWTPc(bQr0>1x?9th^~dU$dN5!*@E-QWMD9=WjG2BPOSjMm zs*|K$>TI$?u6^uQ;1@Sw7rlqh8e2C7D8X@K6;SuK;;=SHHYYJ9Zl!nL+cO>XRXxcX z(d3<qx3Fc=2tIx_eviaV>>`E-rwk#2MMPCJC&D;nq%of5MuY2XA&7!Z4F#sq{XVbI zk3TSE8`Qjg&rX)%=a?J0aY@I#<6d()jQL!ciMw`T#ELNSDHkYjB$<DrquYW+lDx@H zue`!Z8NX!%nF>QA=a5S0&+eX`GpZ?qNd2>r(fwd0*rJ97qRx(7PB!ZJbLVp_zLpNB zVfwyt9%~~!v%PF+JKHGDMYXb;-K9RF;YWSs=l0Kz_hYHkbUqK^u<|BM_C6!~xOEmq zBPA8~HaF8{m_P55G(lnRwS6rl+Qi1*o*$;qxVtzwEtNH#)t>=Ik^5^=N#_>xKz|6j z8_p$=NrN1bGFu)_{mG#+G!^*JJ=CRg7Wnv)4hCgf6dD|SI>8en<P{XULy;2lu$^x@ zWeczAG2ZtCCIjqfGVq?t8)zs0{ruXX@8&;WXYZ=6Zh1E+43u@~a6O%!9%*EBvOH^D z#vze8E=-NWZOv;YvRI%kMtPdQWsYX#ZGRy%-lxgpvzbR|cP}Dy_bmv|KtM*khw4G{ zF=7D%4^*cFOZXB%V4&|vP=k(r5(8cv3>Xvq;`j)5CH2Fo{N0`TJi0tY+bnfDFXZsS zvMbjsolX%?H|H%}ppLD>0ARq02xQKKBj#r(p4sdt&PZ;MRs9Xc-mhPu*`J!Eq?m$l zT{57+aiNUC%^=N!a00#T(G-0a?llGvMx|3Pes_0!DP6A6X5I;(Yn9nx%qBNi>$-eC zGL0&}nu(GWIpn<tqx!4fyH!2nyp1R%m(IvKp?3I<?vqax!%lOJm;;1v1IMiLGuc!R zT&^P6G!A@vDz$#zbj={Q$R50{NE@0?y$CxrqbDQzYy7_W{v>eDcMg$!2|3@cWBr1$ zJt+9J{pK~7kw>VK6F^5Qen2LHAgSiDBq#><g{V+PH}Xa`d0EgwB7N*d0C?e<3=Hn5 z1y2SRF69ALHVL1DIluFPF!iyOd*`vx?uU)J1t!(HJ?i-ypq^kNFVVPPV^#IPLDU@F z@5}})hd=8E08zY4wtZ9xsXBH^g4Mi>t|DU5b6nyS|IyODj_;%9Q*s{^hpl;(*unT$ zGmgt;gU)wDS`rAtnf$Iy^%sPI<=e)fRM;-=8={@YfXi7yzNFZ~8w30k649-LfLKf? zDx-%pfYF~G3g{=70D1IRl7LI<N8T*pabCB4Xe%h!0+jo*5(r!#&KUz-rM~dDezY)9 zO0-w?p8--q(#Z)194VG@NAlE5+(AK6tOqOn5UtvZc5iwyNoytP@QJm>+OL}h#Crp6 zvl7Wq&%OGuMS^<C(pZh0D}{I#d=FHj_}T?(d@)BN3q3sEDbV=e>Lw8}Y%&(MJD8+2 z7Squ^pfn|h2s-!)>rfL0X*veKnf@v>(x?+V)*uqwr=}D>rydAHw03|wW@5x@Zj;XL zrBXFV^U*2jREp}OS}v3Nv#qeKL_2hdvl^r-$B1jUb3+#J?Rhz3)tFIOyb90I8>>kz z!Z91WT_*c?^_XgST~cf3{ehu%swE0;3i`NnveQnQs{sa-l8JSurZP3nq|YJ&nMxl$ zYhS@Z0snH6xOni|ICr4zU2f^riGteLI=WSpmI0>J&1}+|YdhEXQ`ItR2k0kFMN440 zvWRu-%*7bv&Tr1CrpBu#SVr9!Y*vnGJQb}9OTWx83yo;6nn|gFjp72;W4%yX#KlnU z5<H)p0SOE~={lPnOB<7*hfZsZE~NrtOtyxyQ#a_EWVm$8y6GhIN_5`jWalVbaV_J~ z6+XRtnc$g_;gVhU(ze9$^}C<Q#Sed6Tt?Za=uYd)oX{gtEQ5=sxduz{4-Gq)e>Lvv zop`8mNDnm880vS`Rw)l;OS=~~OrC1HnRMTFCT%ojW?3dR9+4C`3b_x!vPZs0EO~2Z zn%l&(m^`2wR9V(ZV>VRvakF3QQLI(<p|YOfc_UG&I)}gOMt2sfMPipDb1qb9jhT43 zNKn*Vd#ioh&GmAFmQg&qJ=DyhhT2gM+2lY43P!IWjq#n#8&uX|FP|5eiexSSeCZO3 z;wzmT{;-%*dsQko6>87eTDwhRIx*%!GEM_LkLzRJMh_mjXL+TBY1a(8TXO|BjxWqA z>En%IY#%d%GUBNXJYB1gzr^+&-52m7S&{D~UjB$TALMzB!f1p*4!b<SgCMrdRtvLe zt<s%Uzt(O%S%2uXZ}mxHu7R~tASo!6k?>mHn|$4L|Atuwr_j}8Bd4W>sA8=~{oHiK z*WSkM9%4DYER|#bvvsxXFh+?nqLUWrxQ<y~e@(+AKe;++Q<}q6dWuA|7hToXL}RYz z*3@Zpxe&&4pmaM#!+KF;On+&6gjw%~UCgVc&8HWRqsNmHDGI6DK&Q&9To&U^hIEfw zabitHcB@B#kve@gna`u8IT$?@QJ+$=<XYZ(qBogMY}oxvm`~3>o&M~^retqQQ4XeJ z=BK85O0E`S-PZfY(J!3HWPHb;Zn|AB(m}=e_;M8)i0sw-Y{wah4(q<+k4DlC=ide% zH(Sb2Sah?mKJa!XV^#+;L)XbF6jVo<8~b*>Uw<{-Q+m2YUBI$BQ9r1;sx;ydo`3Cf zf`6NhSlcsdP=7hdx!t^eTs2j*3mCIZ@+y{fZ~PUBWZAaT?ql55^)3EMdZB|$exvs! zZ;$q3y7LlWel^kczIyga<RQkbl@P<KaVyHkaAF)-w`{aT*OYV5KiBkoEOmP3Nq1I* zq}@}?bqD4c|3@wpr}{e9<0=RAvoLv1C==g{K^c9Q-JN*R7r&z{c%$#5EY~%MFMHxU z%tT<lUI;;79&P)2#p%)95-hXmG5B3}6N#rDrnooBnYOyQC^%B7UCnf~v?)vTgeRM{ zOTFXslbM~fJg3W*?5@21zEN02n>#=w$@H1MwDuN8bZcCD^?k@(ekYp#0G0j5r$a1k zuU8yVZ&nh9dcYSyNB*>xoiDfX1zqFC&jiy>%QDs78n<0vu+b-F(KIzT4sPu1W~F@W z8<y&5e}!dvg=Hn3xJt3Mj;(aOQx)B5E_Crt@4lqUL1V`dRc!Tu?;9kN+-YR&9Xe-e zhxU|PQc7!<W7ltsnshVzuevJ7ztpY-Eip&rs9pO!hiO^|E}WB8$;UZa5v^LSg{dmQ zaH-2J&m1*uf*a>{oJX6OtfChW`w-<)`wx?JOqQg+u49g<)Eh%hDd`x|*R7w)nW;o8 zH>swKsN!`msgiRq<l`m(s%V;{Tjta@S@|8)s6y7kel%Xi0rk*az_iJwmfbmbP+Qo5 zkiM1yLG_D4a*@X?8YA_nyiQ98V`YGQ#nK^MjxW9560@A@dzy-tZcOD2MrqB@K1sY} zvPQX7_44_~yp7FS)D2EKzR1p!K1C{0%ksnRTGfRu)kriI*hwk8rMYjLx@w0#7%8jc z6|=-Dj%7wUI?&McS_h6Sq1p!G>ndE<l(tpXO@&{nZCtZN7@GrerJJUQ2fwe3V~w=1 zQE;_p#VYPlVja4aIqZEusyB!=Y}Y<*NTpnAn65e)TUu**JJ3*RF~&o|I&kFtCjB&L zb@s<Cl{vkAG@KmGA!rcRs_foWoD9*%Z8ZU-&;W=UTHm(O<}cstrM7mOwYX&Y9YIt| zK{U006zXtykyQml#jpY0t`bo(CB=@uine(PK0V;geW9cjUMkfq)@8-Aye0M-x~vW& zz&aJqat4U8l)>pe=!gE)lA;7J#66T5MuncuBblS`GHTkYU&-UUREeIS*12I(LoHmF z<gf=P7K_cxQ>uUW=y3Cha&uo8dK8_RL~#@~F+Gl($zBbZp`WSAgV)(<WO0Kd#KsD( zaD}07I1c2lDQ_6k)M*l!=nu?YFLVFVkyoAqY`BINJ-4Km7P&K!(H97B8A`L)XDMuH zY#6dVn$0A}GHhm|4v|w~-11zRUzGf6Tg62X;Og9z?NvHt>ttX(A++9nI`~^x#S)8{ zm}c!cpPO+v(n^PerCcMpy|zEVle1IG;Ho*jpo%$G!6T=!V@Weg(Za>>yUSTNv3xbU zLsw5btajoF-Uh1bs)kK7rzxj(YK^uSwQ2<Pfln0~8RCUeR2EKrOM-_>bZylLv`*4D zx9=Q}0rsdwYHBLX!)4%2VFsV?x<eV2wX`qnQ?5PiX$Chius444x0QSTs08n@%%4Jo z@rue>?Bd9|K3rPMHpH1oX7iFQZVSLsu_HR8sVJwV?^Vw}s$Hg{y7+bA#CcbQUoR%< zrD?52QcjjoH@SFW)WuLyeqTwAME%|NH181B1&}p-g3|{<E<YCkGS&%oKYd1G_JF&< zUgf8mQFFP^sGh!5qxbRtI59Dj#0-Uq&7*Ihr<Xpi!b{59SY1t5+;{4(r6M}aYmn+> zRq3*U*#e<Rg-6w0j_vk`V?skEZ9DOayvkt(Pt_wq<qUx$M?G-vV{oO+q&nhe0T%0n z-?c=Q!%Wj7M`9=W!!(r3i*sr1qXUk<xZD+e&i!jSYE7@|HN_2PnAIF8YFOx#HH{xd zHR$Gp<nFJL8j;sIC1RVT2Z?FvP-Lj0z`1HfBaOkq9<6t!D4R=`)XsTxLmtfW=Q>7f z(ss<;K*hChx+u~MrxITs5@}Jxs0lvEWujYht{Fkz31Nkh_5MR=!ufxuGXXHM{=2M1 zM`7Hup8>h!l=>l!AmH`$-=wJgB6RORfo*Rf8k+3^nyA0LueQ`QOX-{iQTrz<T~qYd zhnN0<yiRJZ8u$z19yl~brb6?^tkrga6q~dVwH=V+?agjfIrWsW2Aa4a^lXuELv{D< z2(A)bV*B00cCwM+@zv-I{NCMQmoo=7uCx%?LFVR#0}_*i%QYEQ5ZCm3M&<Ai4xb>y z_C2vhH^7=vka`-38M|Os{CK8P?VtJS=iS=+Ea(d(f5KE=UpbNybc2@9MB&^<;avT5 zelqdHb%nojinN06PJ1lVb8jBnLdA=()t#_#mXAtVsu1CV+HHwUjf<ZR;>XK>z{vQQ z-s-<}O#cu%$s1ZKJKOxXGKYobQ@!^;DxBp05-*BbI6654nA!hLyW^s$WtYi-?z>gP z5j!e;g&+7`R4T(nRJpBubSbn9iOzijM;|<<;q?W7{#P*)no!EwrFU~bwt_w$I2gGA zwLa<%UK;4sm4u+>_0A#WNdI<)lLeGIo*$<$fvcA}t4^OMFYnf~6PEv%r?YaDF5!rV z*em#b=VMyA%(WP1S1%C9%g`kj0jWUY+s?1&n<NOU@KY?IXmQ#2((5WEaMwhrh4t)* zu{y`p%ZmaLW3Iiy<0ptC*RQDLqm(k&S9;u`0&q_(kEZ5xPu&+bb=<l``s)QRJ469p z@X7cEYlf;?pdJm4xc&A&21{<}UB4c@bk)%kE7)}Aq{+Q2*Kw!4=PJTK1YIUZVmsLT zTnSft{d!~vLll>2nAqZ^kXMHo)T4&I-qWN=-~O)8oE31G_dSDQFgS=tG_T-NaN6lv z)|x{=PNlCg4pl|g&rIiy+28crDDw27EV#PEP(qeuMnUq>0?CM){Cd?#G*<;na=6lW zvszSb@(jy`y(F{IqKTg~FomrnyNG++o2+ScUeN^f7qV70u8QFEEspbx(b3uX4%Ax$ z1$8`$BaSNLD#_RvNFPmO`~OLh|KP{JNkKnFTmLgT{y$4W|5kW1eM<2DFNruoK*_}_ ohT%a#^S+S$r%LgE(UN|uKMfq6-5pI#VVM}&V9Cft<;7tCA1MXkUH||9 literal 0 HcmV?d00001 diff --git a/test/assets/page-numbers.pdf b/test/assets/page-numbers.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9f5acf40727b01151c314d7e7b45fa692d5882eb GIT binary patch literal 20598 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 T2%#P!15ijb01h{`F$4S)X>v{o literal 0 HcmV?d00001 diff --git a/test/assets/paragraph-merge-1.pdf b/test/assets/paragraph-merge-1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..80ff157912f281036c1f80353024c06f9ca82879 GIT binary patch literal 10631 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 literal 0 HcmV?d00001 diff --git a/test/assets/paragraph-merge-1.pdf.json b/test/assets/paragraph-merge-1.pdf.json new file mode 100644 index 00000000..6e045bec --- /dev/null +++ b/test/assets/paragraph-merge-1.pdf.json @@ -0,0 +1,39 @@ +[ + { + "number": 1, + "pages": 1, + "height": 1262, + "width": 892, + "fonts": [{ "fontspec": "0", "size": "21", "family": "Times", "color": "#161413" }], + "text": [ + { "top": 69, "left": 261, "width": 51, "height": 31, "font": 0, "data": "Lorem " }, + { "top": 69, "left": 316, "width": 48, "height": 31, "font": 0, "data": "ipsum " }, + { "top": 69, "left": 370, "width": 42, "height": 31, "font": 0, "data": "dolor " }, + { "top": 69, "left": 416, "width": 18, "height": 31, "font": 0, "data": "sit " }, + { "top": 69, "left": 438, "width": 43, "height": 31, "font": 0, "data": "amet, " }, + { "top": 69, "left": 487, "width": 106, "height": 31, "font": 0, "data": "consectetuer " }, + { "top": 69, "left": 596, "width": 83, "height": 31, "font": 0, "data": "adipiscing" }, + { "top": 91, "left": 261, "width": 27, "height": 31, "font": 0, "data": "elit. " }, + { "top": 91, "left": 293, "width": 17, "height": 31, "font": 0, "data": "Ut " }, + { "top": 91, "left": 315, "width": 8, "height": 31, "font": 0, "data": "a " }, + { "top": 91, "left": 328, "width": 57, "height": 31, "font": 0, "data": "sapien. " }, + { "top": 91, "left": 389, "width": 66, "height": 31, "font": 0, "data": "Aliquam " }, + { "top": 91, "left": 460, "width": 55, "height": 31, "font": 0, "data": "aliquet " }, + { "top": 91, "left": 520, "width": 46, "height": 31, "font": 0, "data": "purus " }, + { "top": 91, "left": 570, "width": 68, "height": 31, "font": 0, "data": "molestie " }, + { "top": 91, "left": 642, "width": 46, "height": 31, "font": 0, "data": "dolor." }, + { "top": 112, "left": 261, "width": 57, "height": 31, "font": 0, "data": "Integer " }, + { "top": 112, "left": 322, "width": 34, "height": 31, "font": 0, "data": "quis " }, + { "top": 112, "left": 360, "width": 34, "height": 31, "font": 0, "data": "eros " }, + { "top": 112, "left": 399, "width": 16, "height": 31, "font": 0, "data": "ut " }, + { "top": 112, "left": 419, "width": 30, "height": 31, "font": 0, "data": "erat " }, + { "top": 112, "left": 454, "width": 65, "height": 31, "font": 0, "data": "posuere " }, + { "top": 112, "left": 523, "width": 60, "height": 31, "font": 0, "data": "dictum. " }, + { "top": 112, "left": 588, "width": 41, "height": 31, "font": 0, "data": "Nulla " }, + { "top": 112, "left": 634, "width": 38, "height": 31, "font": 0, "data": "vitae" }, + { "top": 134, "left": 261, "width": 49, "height": 31, "font": 0, "data": "turpis. " }, + { "top": 134, "left": 314, "width": 70, "height": 31, "font": 0, "data": "Praesent " }, + { "top": 134, "left": 389, "width": 46, "height": 31, "font": 0, "data": "lacus." } + ] + } +] diff --git a/test/assets/paragraph-merge.pdf b/test/assets/paragraph-merge.pdf new file mode 100644 index 0000000000000000000000000000000000000000..687fce90a0479dcc691c4bce9262a8c5fb26a6b4 GIT binary patch literal 27816 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 Mzp${jiO&B218y4X00000 literal 0 HcmV?d00001 diff --git a/test/assets/paragraph-merge.pdf.json b/test/assets/paragraph-merge.pdf.json new file mode 100644 index 00000000..8a24c38c --- /dev/null +++ b/test/assets/paragraph-merge.pdf.json @@ -0,0 +1,362 @@ +[ + { + "number": 1, + "pages": 1, + "height": 1262, + "width": 892, + "fonts": [{ "fontspec": "0", "size": "21", "family": "Times", "color": "#161413" }], + "text": [ + { "top": 52, "left": 261, "width": 51, "height": 31, "font": 0, "data": "Lorem " }, + { "top": 52, "left": 316, "width": 48, "height": 31, "font": 0, "data": "ipsum " }, + { "top": 52, "left": 370, "width": 42, "height": 31, "font": 0, "data": "dolor " }, + { "top": 52, "left": 416, "width": 18, "height": 31, "font": 0, "data": "sit " }, + { "top": 52, "left": 438, "width": 43, "height": 31, "font": 0, "data": "amet, " }, + { "top": 52, "left": 487, "width": 106, "height": 31, "font": 0, "data": "consectetuer " }, + { "top": 52, "left": 596, "width": 83, "height": 31, "font": 0, "data": "adipiscing" }, + { "top": 74, "left": 261, "width": 27, "height": 31, "font": 0, "data": "elit. " }, + { "top": 74, "left": 293, "width": 17, "height": 31, "font": 0, "data": "Ut " }, + { "top": 74, "left": 315, "width": 8, "height": 31, "font": 0, "data": "a " }, + { "top": 74, "left": 328, "width": 57, "height": 31, "font": 0, "data": "sapien. " }, + { "top": 74, "left": 389, "width": 66, "height": 31, "font": 0, "data": "Aliquam " }, + { "top": 74, "left": 460, "width": 55, "height": 31, "font": 0, "data": "aliquet " }, + { "top": 74, "left": 520, "width": 46, "height": 31, "font": 0, "data": "purus " }, + { "top": 74, "left": 570, "width": 68, "height": 31, "font": 0, "data": "molestie " }, + { "top": 74, "left": 642, "width": 46, "height": 31, "font": 0, "data": "dolor." }, + { "top": 96, "left": 261, "width": 57, "height": 31, "font": 0, "data": "Integer " }, + { "top": 96, "left": 322, "width": 34, "height": 31, "font": 0, "data": "quis " }, + { "top": 96, "left": 360, "width": 34, "height": 31, "font": 0, "data": "eros " }, + { "top": 96, "left": 399, "width": 16, "height": 31, "font": 0, "data": "ut " }, + { "top": 96, "left": 419, "width": 30, "height": 31, "font": 0, "data": "erat " }, + { "top": 96, "left": 454, "width": 65, "height": 31, "font": 0, "data": "posuere " }, + { "top": 96, "left": 523, "width": 60, "height": 31, "font": 0, "data": "dictum. " }, + { "top": 96, "left": 588, "width": 76, "height": 31, "font": 0, "data": "Curabitur" }, + { "top": 117, "left": 261, "width": 81, "height": 31, "font": 0, "data": "dignissim. " }, + { "top": 117, "left": 346, "width": 57, "height": 31, "font": 0, "data": "Integer " }, + { "top": 117, "left": 408, "width": 33, "height": 31, "font": 0, "data": "orci. " }, + { "top": 117, "left": 445, "width": 48, "height": 31, "font": 0, "data": "Fusce " }, + { "top": 117, "left": 498, "width": 76, "height": 31, "font": 0, "data": "vulputate " }, + { "top": 117, "left": 578, "width": 42, "height": 31, "font": 0, "data": "lacus " }, + { "top": 117, "left": 625, "width": 15, "height": 31, "font": 0, "data": "at " }, + { "top": 117, "left": 644, "width": 53, "height": 31, "font": 0, "data": "ipsum." }, + { "top": 139, "left": 261, "width": 67, "height": 31, "font": 0, "data": "Quisque " }, + { "top": 139, "left": 333, "width": 14, "height": 31, "font": 0, "data": "in " }, + { "top": 139, "left": 351, "width": 45, "height": 31, "font": 0, "data": "libero " }, + { "top": 139, "left": 401, "width": 30, "height": 31, "font": 0, "data": "nec " }, + { "top": 139, "left": 435, "width": 18, "height": 31, "font": 0, "data": "mi " }, + { "top": 139, "left": 459, "width": 55, "height": 31, "font": 0, "data": "laoreet " }, + { "top": 139, "left": 519, "width": 70, "height": 31, "font": 0, "data": "volutpat. " }, + { "top": 139, "left": 593, "width": 66, "height": 31, "font": 0, "data": "Aliquam " }, + { "top": 139, "left": 664, "width": 34, "height": 31, "font": 0, "data": "eros" }, + { "top": 160, "left": 261, "width": 44, "height": 31, "font": 0, "data": "pede, " }, + { "top": 160, "left": 310, "width": 91, "height": 31, "font": 0, "data": "scelerisque " }, + { "top": 160, "left": 406, "width": 37, "height": 31, "font": 0, "data": "quis, " }, + { "top": 160, "left": 448, "width": 65, "height": 31, "font": 0, "data": "tristique " }, + { "top": 160, "left": 518, "width": 56, "height": 31, "font": 0, "data": "cursus, " }, + { "top": 160, "left": 579, "width": 65, "height": 31, "font": 0, "data": "placerat" }, + { "top": 182, "left": 261, "width": 74, "height": 31, "font": 0, "data": "convallis, " }, + { "top": 182, "left": 339, "width": 36, "height": 31, "font": 0, "data": "velit. " }, + { "top": 182, "left": 381, "width": 37, "height": 31, "font": 0, "data": "Nam " }, + { "top": 182, "left": 422, "width": 117, "height": 31, "font": 0, "data": "condimentum. " }, + { "top": 182, "left": 544, "width": 41, "height": 31, "font": 0, "data": "Nulla " }, + { "top": 182, "left": 590, "width": 16, "height": 31, "font": 0, "data": "ut " }, + { "top": 182, "left": 610, "width": 57, "height": 31, "font": 0, "data": "mauris." }, + { "top": 204, "left": 261, "width": 76, "height": 31, "font": 0, "data": "Curabitur " }, + { "top": 204, "left": 341, "width": 87, "height": 31, "font": 0, "data": "adipiscing, " }, + { "top": 204, "left": 433, "width": 54, "height": 31, "font": 0, "data": "mauris " }, + { "top": 204, "left": 491, "width": 31, "height": 31, "font": 0, "data": "non " }, + { "top": 204, "left": 526, "width": 55, "height": 31, "font": 0, "data": "dictum " }, + { "top": 204, "left": 587, "width": 67, "height": 31, "font": 0, "data": "aliquam, " }, + { "top": 204, "left": 660, "width": 34, "height": 31, "font": 0, "data": "arcu" }, + { "top": 225, "left": 261, "width": 37, "height": 31, "font": 0, "data": "risus " }, + { "top": 225, "left": 302, "width": 65, "height": 31, "font": 0, "data": "dapibus " }, + { "top": 225, "left": 372, "width": 43, "height": 31, "font": 0, "data": "diam, " }, + { "top": 225, "left": 420, "width": 30, "height": 31, "font": 0, "data": "nec " }, + { "top": 225, "left": 454, "width": 87, "height": 31, "font": 0, "data": "sollicitudin " }, + { "top": 225, "left": 546, "width": 45, "height": 31, "font": 0, "data": "quam " }, + { "top": 225, "left": 596, "width": 30, "height": 31, "font": 0, "data": "erat " }, + { "top": 225, "left": 631, "width": 34, "height": 31, "font": 0, "data": "quis" }, + { "top": 247, "left": 261, "width": 48, "height": 31, "font": 0, "data": "ligula. " }, + { "top": 247, "left": 313, "width": 61, "height": 31, "font": 0, "data": "Aenean " }, + { "top": 247, "left": 379, "width": 50, "height": 31, "font": 0, "data": "massa " }, + { "top": 247, "left": 434, "width": 43, "height": 31, "font": 0, "data": "nulla, " }, + { "top": 247, "left": 482, "width": 66, "height": 31, "font": 0, "data": "volutpat " }, + { "top": 247, "left": 553, "width": 23, "height": 31, "font": 0, "data": "eu, " }, + { "top": 247, "left": 581, "width": 82, "height": 31, "font": 0, "data": "accumsan " }, + { "top": 247, "left": 668, "width": 18, "height": 31, "font": 0, "data": "et," }, + { "top": 268, "left": 261, "width": 59, "height": 31, "font": 0, "data": "fringilla " }, + { "top": 268, "left": 325, "width": 39, "height": 31, "font": 0, "data": "eget, " }, + { "top": 268, "left": 369, "width": 39, "height": 31, "font": 0, "data": "odio. " }, + { "top": 268, "left": 413, "width": 41, "height": 31, "font": 0, "data": "Nulla " }, + { "top": 268, "left": 460, "width": 65, "height": 31, "font": 0, "data": "placerat " }, + { "top": 268, "left": 529, "width": 41, "height": 31, "font": 0, "data": "porta " }, + { "top": 268, "left": 576, "width": 42, "height": 31, "font": 0, "data": "justo. " }, + { "top": 268, "left": 623, "width": 41, "height": 31, "font": 0, "data": "Nulla" }, + { "top": 290, "left": 261, "width": 38, "height": 31, "font": 0, "data": "vitae " }, + { "top": 290, "left": 303, "width": 49, "height": 31, "font": 0, "data": "turpis. " }, + { "top": 290, "left": 357, "width": 70, "height": 31, "font": 0, "data": "Praesent " }, + { "top": 290, "left": 431, "width": 46, "height": 31, "font": 0, "data": "lacus." }, + { "top": 334, "left": 341, "width": 41, "height": 31, "font": 0, "data": "Nulla " }, + { "top": 334, "left": 387, "width": 53, "height": 31, "font": 0, "data": "facilisi. " }, + { "top": 334, "left": 445, "width": 37, "height": 31, "font": 0, "data": "Nam " }, + { "top": 334, "left": 487, "width": 47, "height": 31, "font": 0, "data": "varius " }, + { "top": 334, "left": 539, "width": 35, "height": 31, "font": 0, "data": "ante " }, + { "top": 334, "left": 578, "width": 76, "height": 31, "font": 0, "data": "dignissim " }, + { "top": 334, "left": 660, "width": 38, "height": 31, "font": 0, "data": "arcu." }, + { "top": 356, "left": 276, "width": 102, "height": 31, "font": 0, "data": "Suspendisse " }, + { "top": 356, "left": 383, "width": 68, "height": 31, "font": 0, "data": "molestie " }, + { "top": 356, "left": 455, "width": 76, "height": 31, "font": 0, "data": "dignissim " }, + { "top": 356, "left": 536, "width": 55, "height": 31, "font": 0, "data": "neque. " }, + { "top": 356, "left": 596, "width": 102, "height": 31, "font": 0, "data": "Suspendisse" }, + { "top": 377, "left": 275, "width": 24, "height": 31, "font": 0, "data": "leo " }, + { "top": 377, "left": 304, "width": 52, "height": 31, "font": 0, "data": "ipsum, " }, + { "top": 377, "left": 361, "width": 53, "height": 31, "font": 0, "data": "rutrum " }, + { "top": 377, "left": 419, "width": 56, "height": 31, "font": 0, "data": "cursus, " }, + { "top": 377, "left": 481, "width": 87, "height": 31, "font": 0, "data": "malesuada " }, + { "top": 377, "left": 573, "width": 18, "height": 31, "font": 0, "data": "id, " }, + { "top": 377, "left": 596, "width": 65, "height": 31, "font": 0, "data": "dapibus " }, + { "top": 377, "left": 666, "width": 32, "height": 31, "font": 0, "data": "sed," }, + { "top": 399, "left": 273, "width": 40, "height": 31, "font": 0, "data": "urna. " }, + { "top": 399, "left": 317, "width": 48, "height": 31, "font": 0, "data": "Fusce " }, + { "top": 399, "left": 370, "width": 87, "height": 31, "font": 0, "data": "sollicitudin " }, + { "top": 399, "left": 462, "width": 55, "height": 31, "font": 0, "data": "laoreet " }, + { "top": 399, "left": 522, "width": 43, "height": 31, "font": 0, "data": "diam. " }, + { "top": 399, "left": 570, "width": 54, "height": 31, "font": 0, "data": "Mauris " }, + { "top": 399, "left": 628, "width": 19, "height": 31, "font": 0, "data": "eu " }, + { "top": 399, "left": 652, "width": 45, "height": 31, "font": 0, "data": "quam" }, + { "top": 421, "left": 300, "width": 36, "height": 31, "font": 0, "data": "eget " }, + { "top": 421, "left": 341, "width": 39, "height": 31, "font": 0, "data": "nulla " }, + { "top": 421, "left": 385, "width": 87, "height": 31, "font": 0, "data": "fermentum " }, + { "top": 421, "left": 477, "width": 87, "height": 31, "font": 0, "data": "adipiscing. " }, + { "top": 421, "left": 569, "width": 14, "height": 31, "font": 0, "data": "In " }, + { "top": 421, "left": 588, "width": 29, "height": 31, "font": 0, "data": "hac " }, + { "top": 421, "left": 622, "width": 76, "height": 31, "font": 0, "data": "habitasse" }, + { "top": 442, "left": 268, "width": 49, "height": 31, "font": 0, "data": "platea " }, + { "top": 442, "left": 322, "width": 73, "height": 31, "font": 0, "data": "dictumst. " }, + { "top": 442, "left": 400, "width": 46, "height": 31, "font": 0, "data": "Morbi " }, + { "top": 442, "left": 451, "width": 16, "height": 31, "font": 0, "data": "ut " }, + { "top": 442, "left": 472, "width": 36, "height": 31, "font": 0, "data": "odio " }, + { "top": 442, "left": 512, "width": 38, "height": 31, "font": 0, "data": "vitae " }, + { "top": 442, "left": 554, "width": 34, "height": 31, "font": 0, "data": "eros " }, + { "top": 442, "left": 593, "width": 49, "height": 31, "font": 0, "data": "luctus " }, + { "top": 442, "left": 646, "width": 52, "height": 31, "font": 0, "data": "luctus." }, + { "top": 464, "left": 330, "width": 17, "height": 31, "font": 0, "data": "Ut " }, + { "top": 464, "left": 352, "width": 43, "height": 31, "font": 0, "data": "diam. " }, + { "top": 464, "left": 400, "width": 76, "height": 31, "font": 0, "data": "Phasellus " }, + { "top": 464, "left": 481, "width": 97, "height": 31, "font": 0, "data": "ullamcorper " }, + { "top": 464, "left": 582, "width": 34, "height": 31, "font": 0, "data": "arcu " }, + { "top": 464, "left": 622, "width": 38, "height": 31, "font": 0, "data": "vitae " }, + { "top": 464, "left": 664, "width": 34, "height": 31, "font": 0, "data": "wisi." }, + { "top": 485, "left": 289, "width": 105, "height": 31, "font": 0, "data": "Pellentesque " }, + { "top": 485, "left": 398, "width": 36, "height": 31, "font": 0, "data": "urna " }, + { "top": 485, "left": 439, "width": 39, "height": 31, "font": 0, "data": "odio, " }, + { "top": 485, "left": 483, "width": 47, "height": 31, "font": 0, "data": "varius " }, + { "top": 485, "left": 535, "width": 39, "height": 31, "font": 0, "data": "eget, " }, + { "top": 485, "left": 579, "width": 76, "height": 31, "font": 0, "data": "dignissim " }, + { "top": 485, "left": 661, "width": 37, "height": 31, "font": 0, "data": "quis," }, + { "top": 507, "left": 316, "width": 67, "height": 31, "font": 0, "data": "vehicula " }, + { "top": 507, "left": 388, "width": 68, "height": 31, "font": 0, "data": "placerat, " }, + { "top": 507, "left": 462, "width": 44, "height": 31, "font": 0, "data": "nunc. " }, + { "top": 507, "left": 511, "width": 17, "height": 31, "font": 0, "data": "Ut " }, + { "top": 507, "left": 533, "width": 30, "height": 31, "font": 0, "data": "nec " }, + { "top": 507, "left": 567, "width": 49, "height": 31, "font": 0, "data": "metus " }, + { "top": 507, "left": 621, "width": 34, "height": 31, "font": 0, "data": "quis " }, + { "top": 507, "left": 659, "width": 39, "height": 31, "font": 0, "data": "nulla" }, + { "top": 529, "left": 560, "width": 65, "height": 31, "font": 0, "data": "posuere " }, + { "top": 529, "left": 630, "width": 68, "height": 31, "font": 0, "data": "eleifend." }, + { "top": 573, "left": 268, "width": 68, "height": 31, "font": 0, "data": "Vivamus " }, + { "top": 573, "left": 340, "width": 51, "height": 31, "font": 0, "data": "neque " }, + { "top": 573, "left": 396, "width": 36, "height": 31, "font": 0, "data": "velit, " }, + { "top": 573, "left": 437, "width": 52, "height": 31, "font": 0, "data": "ornare " }, + { "top": 573, "left": 494, "width": 41, "height": 31, "font": 0, "data": "vitae, " }, + { "top": 573, "left": 540, "width": 58, "height": 31, "font": 0, "data": "tempor " }, + { "top": 573, "left": 602, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 573, "left": 633, "width": 58, "height": 31, "font": 0, "data": "ultrices" }, + { "top": 594, "left": 267, "width": 18, "height": 31, "font": 0, "data": "et, " }, + { "top": 594, "left": 291, "width": 34, "height": 31, "font": 0, "data": "wisi. " }, + { "top": 594, "left": 329, "width": 36, "height": 31, "font": 0, "data": "Cras " }, + { "top": 594, "left": 370, "width": 45, "height": 31, "font": 0, "data": "pede. " }, + { "top": 594, "left": 419, "width": 76, "height": 31, "font": 0, "data": "Phasellus " }, + { "top": 594, "left": 500, "width": 41, "height": 31, "font": 0, "data": "nunc " }, + { "top": 594, "left": 546, "width": 48, "height": 31, "font": 0, "data": "turpis, " }, + { "top": 594, "left": 599, "width": 53, "height": 31, "font": 0, "data": "cursus " }, + { "top": 594, "left": 656, "width": 34, "height": 31, "font": 0, "data": "non," }, + { "top": 616, "left": 341, "width": 66, "height": 31, "font": 0, "data": "rhoncus " }, + { "top": 616, "left": 411, "width": 41, "height": 31, "font": 0, "data": "vitae, " }, + { "top": 616, "left": 458, "width": 87, "height": 31, "font": 0, "data": "sollicitudin " }, + { "top": 616, "left": 550, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 616, "left": 581, "width": 36, "height": 31, "font": 0, "data": "velit." }, + { + "top": 637, + "left": 261, + "width": 175, + "height": 31, + "font": 0, + "data": "Vivamussuscipitlorem " + }, + { "top": 637, "left": 441, "width": 28, "height": 31, "font": 0, "data": "sed " }, + { "top": 637, "left": 474, "width": 35, "height": 31, "font": 0, "data": "felis. " }, + { "top": 637, "left": 514, "width": 90, "height": 31, "font": 0, "data": "Vestibulum " }, + { "top": 637, "left": 609, "width": 88, "height": 31, "font": 0, "data": "vestibulum" }, + { "top": 659, "left": 311, "width": 58, "height": 31, "font": 0, "data": "ultrices " }, + { "top": 659, "left": 373, "width": 49, "height": 31, "font": 0, "data": "turpis. " }, + { "top": 659, "left": 427, "width": 51, "height": 31, "font": 0, "data": "Lorem " }, + { "top": 659, "left": 483, "width": 48, "height": 31, "font": 0, "data": "ipsum " }, + { "top": 659, "left": 536, "width": 42, "height": 31, "font": 0, "data": "dolor " }, + { "top": 659, "left": 582, "width": 18, "height": 31, "font": 0, "data": "sit " }, + { "top": 659, "left": 605, "width": 43, "height": 31, "font": 0, "data": "amet," }, + { "top": 681, "left": 367, "width": 106, "height": 31, "font": 0, "data": "consectetuer " }, + { "top": 681, "left": 477, "width": 83, "height": 31, "font": 0, "data": "adipiscing " }, + { "top": 681, "left": 565, "width": 27, "height": 31, "font": 0, "data": "elit." }, + { + "top": 702, + "left": 274, + "width": 235, + "height": 31, + "font": 0, + "data": "Praesentornarenullanecjusto. " + }, + { "top": 702, "left": 514, "width": 30, "height": 31, "font": 0, "data": "Sed " }, + { "top": 702, "left": 550, "width": 30, "height": 31, "font": 0, "data": "nec " }, + { "top": 702, "left": 584, "width": 37, "height": 31, "font": 0, "data": "risus " }, + { "top": 702, "left": 625, "width": 19, "height": 31, "font": 0, "data": "ac " }, + { "top": 702, "left": 648, "width": 37, "height": 31, "font": 0, "data": "risus" }, + { "top": 724, "left": 284, "width": 87, "height": 31, "font": 0, "data": "fermentum " }, + { "top": 724, "left": 376, "width": 92, "height": 31, "font": 0, "data": "vestibulum. " }, + { "top": 724, "left": 473, "width": 44, "height": 31, "font": 0, "data": "Etiam " }, + { "top": 724, "left": 523, "width": 53, "height": 31, "font": 0, "data": "viverra " }, + { "top": 724, "left": 581, "width": 53, "height": 31, "font": 0, "data": "viverra " }, + { "top": 724, "left": 638, "width": 37, "height": 31, "font": 0, "data": "sem." }, + { "top": 745, "left": 288, "width": 44, "height": 31, "font": 0, "data": "Etiam " }, + { "top": 745, "left": 337, "width": 68, "height": 31, "font": 0, "data": "molestie " }, + { "top": 745, "left": 409, "width": 18, "height": 31, "font": 0, "data": "mi " }, + { "top": 745, "left": 433, "width": 34, "height": 31, "font": 0, "data": "quis " }, + { "top": 745, "left": 471, "width": 49, "height": 31, "font": 0, "data": "metus " }, + { "top": 745, "left": 525, "width": 73, "height": 31, "font": 0, "data": "hendrerit " }, + { "top": 745, "left": 602, "width": 69, "height": 31, "font": 0, "data": "tristique." }, + { "top": 790, "left": 261, "width": 37, "height": 31, "font": 0, "data": "Nam " }, + { "top": 790, "left": 304, "width": 50, "height": 31, "font": 0, "data": "iaculis " }, + { "top": 790, "left": 360, "width": 56, "height": 31, "font": 0, "data": "blandit " }, + { "top": 790, "left": 422, "width": 49, "height": 31, "font": 0, "data": "purus. " }, + { "top": 790, "left": 478, "width": 54, "height": 31, "font": 0, "data": "Mauris " }, + { "top": 790, "left": 538, "width": 36, "height": 31, "font": 0, "data": "odio " }, + { "top": 790, "left": 579, "width": 39, "height": 31, "font": 0, "data": "nibh, " }, + { "top": 790, "left": 625, "width": 73, "height": 31, "font": 0, "data": "hendrerit" }, + { "top": 811, "left": 261, "width": 18, "height": 31, "font": 0, "data": "id, " }, + { "top": 811, "left": 289, "width": 53, "height": 31, "font": 0, "data": "cursus " }, + { "top": 811, "left": 351, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 811, "left": 387, "width": 56, "height": 31, "font": 0, "data": "sagittis " }, + { "top": 811, "left": 453, "width": 12, "height": 31, "font": 0, "data": "a, " }, + { "top": 811, "left": 476, "width": 46, "height": 31, "font": 0, "data": "dolor. " }, + { "top": 811, "left": 531, "width": 56, "height": 31, "font": 0, "data": "Nullam " }, + { "top": 811, "left": 598, "width": 45, "height": 31, "font": 0, "data": "turpis " }, + { "top": 811, "left": 652, "width": 45, "height": 31, "font": 0, "data": "lacus," }, + { "top": 833, "left": 261, "width": 58, "height": 31, "font": 0, "data": "ultrices " }, + { "top": 833, "left": 323, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 833, "left": 355, "width": 56, "height": 31, "font": 0, "data": "sagittis " }, + { "top": 833, "left": 416, "width": 41, "height": 31, "font": 0, "data": "vitae, " }, + { "top": 833, "left": 463, "width": 65, "height": 31, "font": 0, "data": "dapibus " }, + { "top": 833, "left": 532, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 833, "left": 564, "width": 27, "height": 31, "font": 0, "data": "elit. " }, + { "top": 833, "left": 596, "width": 102, "height": 31, "font": 0, "data": "Suspendisse" }, + { "top": 854, "left": 261, "width": 54, "height": 31, "font": 0, "data": "auctor, " }, + { "top": 854, "left": 333, "width": 52, "height": 31, "font": 0, "data": "sapien " }, + { "top": 854, "left": 404, "width": 15, "height": 31, "font": 0, "data": "et " }, + { "top": 854, "left": 437, "width": 61, "height": 31, "font": 0, "data": "suscipit " }, + { "top": 854, "left": 516, "width": 61, "height": 31, "font": 0, "data": "tempor, " }, + { "top": 854, "left": 596, "width": 45, "height": 31, "font": 0, "data": "turpis " }, + { "top": 854, "left": 659, "width": 39, "height": 31, "font": 0, "data": "enim" }, + { "top": 876, "left": 261, "width": 85, "height": 31, "font": 0, "data": "consequat " }, + { "top": 876, "left": 358, "width": 36, "height": 31, "font": 0, "data": "sem, " }, + { "top": 876, "left": 407, "width": 19, "height": 31, "font": 0, "data": "eu " }, + { "top": 876, "left": 439, "width": 55, "height": 31, "font": 0, "data": "dictum " }, + { "top": 876, "left": 507, "width": 41, "height": 31, "font": 0, "data": "nunc " }, + { "top": 876, "left": 560, "width": 45, "height": 31, "font": 0, "data": "lorem " }, + { "top": 876, "left": 617, "width": 15, "height": 31, "font": 0, "data": "at " }, + { "top": 876, "left": 644, "width": 54, "height": 31, "font": 0, "data": "massa." }, + { "top": 898, "left": 261, "width": 105, "height": 31, "font": 0, "data": "Pellentesque " }, + { "top": 898, "left": 380, "width": 91, "height": 31, "font": 0, "data": "scelerisque " }, + { "top": 898, "left": 487, "width": 49, "height": 31, "font": 0, "data": "purus. " }, + { "top": 898, "left": 551, "width": 44, "height": 31, "font": 0, "data": "Etiam " }, + { "top": 898, "left": 611, "width": 28, "height": 31, "font": 0, "data": "sed " }, + { "top": 898, "left": 655, "width": 43, "height": 31, "font": 0, "data": "enim." }, + { "top": 919, "left": 261, "width": 82, "height": 31, "font": 0, "data": "Maecenas " }, + { "top": 919, "left": 351, "width": 28, "height": 31, "font": 0, "data": "sed " }, + { "top": 919, "left": 388, "width": 44, "height": 31, "font": 0, "data": "tortor " }, + { "top": 919, "left": 439, "width": 14, "height": 31, "font": 0, "data": "id " }, + { "top": 919, "left": 462, "width": 45, "height": 31, "font": 0, "data": "turpis " }, + { "top": 919, "left": 516, "width": 85, "height": 31, "font": 0, "data": "consequat " }, + { "top": 919, "left": 609, "width": 89, "height": 31, "font": 0, "data": "consequat." }, + { "top": 941, "left": 261, "width": 76, "height": 31, "font": 0, "data": "Curabitur " }, + { "top": 941, "left": 341, "width": 63, "height": 31, "font": 0, "data": "fringilla. " }, + { "top": 941, "left": 409, "width": 30, "height": 31, "font": 0, "data": "Sed " }, + { "top": 941, "left": 445, "width": 37, "height": 31, "font": 0, "data": "risus " }, + { "top": 941, "left": 486, "width": 33, "height": 31, "font": 0, "data": "wisi, " }, + { "top": 941, "left": 526, "width": 55, "height": 31, "font": 0, "data": "dictum " }, + { "top": 941, "left": 586, "width": 12, "height": 31, "font": 0, "data": "a, " }, + { "top": 941, "left": 604, "width": 56, "height": 31, "font": 0, "data": "sagittis " }, + { "top": 941, "left": 665, "width": 33, "height": 31, "font": 0, "data": "nec," }, + { "top": 962, "left": 261, "width": 49, "height": 31, "font": 0, "data": "luctus " }, + { "top": 962, "left": 325, "width": 22, "height": 31, "font": 0, "data": "ac, " }, + { "top": 962, "left": 362, "width": 55, "height": 31, "font": 0, "data": "neque. " }, + { "top": 962, "left": 433, "width": 51, "height": 31, "font": 0, "data": "Lorem " }, + { "top": 962, "left": 500, "width": 48, "height": 31, "font": 0, "data": "ipsum " }, + { "top": 962, "left": 564, "width": 42, "height": 31, "font": 0, "data": "dolor " }, + { "top": 962, "left": 622, "width": 18, "height": 31, "font": 0, "data": "sit " }, + { "top": 962, "left": 655, "width": 43, "height": 31, "font": 0, "data": "amet," }, + { "top": 984, "left": 261, "width": 106, "height": 31, "font": 0, "data": "consectetuer " }, + { "top": 984, "left": 372, "width": 83, "height": 31, "font": 0, "data": "adipiscing " }, + { "top": 984, "left": 461, "width": 27, "height": 31, "font": 0, "data": "elit. " }, + { "top": 984, "left": 494, "width": 30, "height": 31, "font": 0, "data": "Sed " }, + { "top": 984, "left": 531, "width": 35, "height": 31, "font": 0, "data": "nibh " }, + { "top": 984, "left": 573, "width": 55, "height": 31, "font": 0, "data": "neque, " }, + { "top": 984, "left": 634, "width": 64, "height": 31, "font": 0, "data": "aliquam" }, + { "top": 1006, "left": 261, "width": 19, "height": 31, "font": 0, "data": "ut, " }, + { "top": 1006, "left": 290, "width": 56, "height": 31, "font": 0, "data": "sagittis " }, + { "top": 1006, "left": 357, "width": 18, "height": 31, "font": 0, "data": "id, " }, + { "top": 1006, "left": 385, "width": 60, "height": 31, "font": 0, "data": "gravida " }, + { "top": 1006, "left": 456, "width": 18, "height": 31, "font": 0, "data": "et, " }, + { "top": 1006, "left": 484, "width": 27, "height": 31, "font": 0, "data": "est. " }, + { "top": 1006, "left": 522, "width": 61, "height": 31, "font": 0, "data": "Aenean " }, + { "top": 1006, "left": 593, "width": 106, "height": 31, "font": 0, "data": "consectetuer" }, + { "top": 1027, "left": 261, "width": 61, "height": 31, "font": 0, "data": "pretium " }, + { "top": 1027, "left": 331, "width": 43, "height": 31, "font": 0, "data": "enim. " }, + { "top": 1027, "left": 384, "width": 61, "height": 31, "font": 0, "data": "Aenean " }, + { "top": 1027, "left": 454, "width": 43, "height": 31, "font": 0, "data": "tellus " }, + { "top": 1027, "left": 505, "width": 49, "height": 31, "font": 0, "data": "quam, " }, + { "top": 1027, "left": 564, "width": 112, "height": 31, "font": 0, "data": "condimentum " }, + { "top": 1027, "left": 685, "width": 12, "height": 31, "font": 0, "data": "a," }, + { "top": 1049, "left": 261, "width": 83, "height": 31, "font": 0, "data": "adipiscing " }, + { "top": 1049, "left": 357, "width": 18, "height": 31, "font": 0, "data": "et, " }, + { "top": 1049, "left": 389, "width": 51, "height": 31, "font": 0, "data": "lacinia " }, + { "top": 1049, "left": 455, "width": 26, "height": 31, "font": 0, "data": "vel, " }, + { "top": 1049, "left": 495, "width": 38, "height": 31, "font": 0, "data": "ante. " }, + { "top": 1049, "left": 547, "width": 70, "height": 31, "font": 0, "data": "Praesent " }, + { "top": 1049, "left": 630, "width": 69, "height": 31, "font": 0, "data": "faucibus" }, + { "top": 1070, "left": 261, "width": 76, "height": 31, "font": 0, "data": "dignissim " }, + { "top": 1070, "left": 347, "width": 43, "height": 31, "font": 0, "data": "enim. " }, + { "top": 1070, "left": 401, "width": 66, "height": 31, "font": 0, "data": "Aliquam " }, + { "top": 1070, "left": 477, "width": 75, "height": 31, "font": 0, "data": "tincidunt. " }, + { "top": 1070, "left": 562, "width": 54, "height": 31, "font": 0, "data": "Mauris " }, + { "top": 1070, "left": 626, "width": 24, "height": 31, "font": 0, "data": "leo " }, + { "top": 1070, "left": 660, "width": 38, "height": 31, "font": 0, "data": "ante," }, + { "top": 1092, "left": 261, "width": 112, "height": 31, "font": 0, "data": "condimentum " }, + { "top": 1092, "left": 380, "width": 39, "height": 31, "font": 0, "data": "eget, " }, + { "top": 1092, "left": 425, "width": 88, "height": 31, "font": 0, "data": "vestibulum " }, + { "top": 1092, "left": 520, "width": 18, "height": 31, "font": 0, "data": "sit " }, + { "top": 1092, "left": 543, "width": 43, "height": 31, "font": 0, "data": "amet, " }, + { "top": 1092, "left": 593, "width": 59, "height": 31, "font": 0, "data": "fringilla " }, + { "top": 1092, "left": 659, "width": 39, "height": 31, "font": 0, "data": "eget," }, + { "top": 1114, "left": 261, "width": 43, "height": 31, "font": 0, "data": "diam. " }, + { "top": 1114, "left": 310, "width": 37, "height": 31, "font": 0, "data": "Nam " }, + { "top": 1114, "left": 353, "width": 62, "height": 31, "font": 0, "data": "ultricies " }, + { "top": 1114, "left": 421, "width": 97, "height": 31, "font": 0, "data": "ullamcorper " }, + { "top": 1114, "left": 524, "width": 40, "height": 31, "font": 0, "data": "nibh. " }, + { "top": 1114, "left": 569, "width": 44, "height": 31, "font": 0, "data": "Etiam " }, + { "top": 1114, "left": 620, "width": 55, "height": 31, "font": 0, "data": "neque. " }, + { "top": 1114, "left": 681, "width": 17, "height": 31, "font": 0, "data": "Ut" }, + { "top": 1135, "left": 261, "width": 65, "height": 31, "font": 0, "data": "posuere" }, + { "top": 1135, "left": 465, "width": 55, "height": 31, "font": 0, "data": "laoreet" }, + { "top": 1135, "left": 659, "width": 39, "height": 31, "font": 0, "data": "fade." } + ] + } +] diff --git a/test/assets/redundancy-detection.pdf b/test/assets/redundancy-detection.pdf new file mode 100644 index 0000000000000000000000000000000000000000..046fb24fcae0b6bd57731599df3c6c4c3e35fa73 GIT binary patch literal 13856 zcmaKT1yCIAvNj3smH<IFxWfXAySux)JBz!!yF-FQg1bAxHNh>oOYlJc<va4-d(K~X zYO7}6p6+?OpYERB+IpHoQB<4($jF97F?W2pg#-jJ100O4k$8ETM4Zixzz)s;YB>il zD_dJ5CJ-YtK$lw1$i&JX?BHU_1CX)@o7n;&0)T=FKobCDHUP320Ci~qLXM8MW*TP3 zGFD(F5Gw~GD?7wcMp9KymL6bhWn%`AFf*}nfGCPsIy=~zF>$amGDDO&Ss8)MtN;}w zb0cS~e;U#tF)2Dbn7W#nK|E2hu`*(UI1I4kV1?ZH%R?&%dr>2>8Gu@pn}r$3#moi- zF>`@ffXsBv%v6vML{HAa^#4}j=SMQLH+_wp_3!vii~v9+CSf@Mle~koossR|N}#`$ z#I0<>5YL#zZ6Q90nwdD5njtaCn%P@`Edf9-R(^hf3mAf<9g;`Ru0k(TA2UklKAkhp zNm7%R4jwUFTfOoH^gL<^;^ZIrxtjv`KRrdoOowCMUuqe@1QxY|49tF+CKb(Zn}ty^ zH<v6R;B*%>3Ln^Fh=}5(6XbB*E4Hy#X%uUO2rM{!a5bd&#IFf53o>`)>?|0Eml@eH z0pk5Ydv6UZeagxPUya_&TQU2HI~0KhoSMeua=lY>2Vn7q(%p6$E6K`hEE;t|hnwb* z<El{`)K>P-;%-YX?ss4ET6evH>ad@EMd+Wg{1YpdzhO}IbTk7nDH>ToqE|L^ad34u zF>?VxiU^WB6BRSC3qa?uyfLYod4K^-Qg#qc;eVb*{&|+tLt+y1086NVU(*S(m0$(D zrb8J5Jd?PCJ@{27&IVxnhwN1<XJ%?;B<$b;&|!uML0oK%Tp%tkb^s?E^FOX2j}(De zn%P4<c+HwJ5|awp+0_KBXyp8tkQq{5e_aCD|GCvagL_^6vl0Fd&gJ!St`JWkg&|{Q z3W@3!17*FxOM~M-%|wjAMz#(XuiIEaAo-77IR94vN6`QQEFgAPc1XbgS78A`K+gYJ zTBkmUhN@z-j;?pvWW%!N&J#NThd9|d#BVz=!f!J}U=Y$*$t42>;jj>3k^(jZ%K%vK zgw&lD#BgTRFcC399(56<QOBX1ZKTwZ?xyKtq8~G>+cwByP>$~WUpDSYipn)^+A7;C zD=RCGJ3F9YpxzpCF-z(x9&2H^A4OUR_%T5Fk&H9t?=Ll;RHj3fFv2U^<!W@92A*xa zz`u$3;H_0*W1d#(llsWjlkk&sWO~%|M&>N%{uagd93I2Z+pM^hw`G|b!&8B5VB1d2 zMyC6-WOu-{h1<=n);{f2H56WYz_W}wLk3?pwfB$(l$FJsd4nlkT?fyr9uff<YC*qL zyS<Jp*kg|P1Sr`jDAKvWu{Z(GA4GBaI4MtjK<mNhwD<5Z{J-;_7vfT`nGRikmjbE{ zs04<s4+Mn`))HqEKTRE4Up|)+Z-YE?!(!-!kl&$bMnSO|Jo-XmVYsq$a#Ed!3~zn3 z`UsTV<cog0S3tf$AKy=yg{ok*q6&li{fDHxF#eT5$Xzn*<ZWJ$*Lu_2-Z%gmCq@$H z<HHdP=IlM8aUPG*_D=*9W`%l;rpnu35fEloL^zqKi#GQg^%NJ*^wgU^?{_4JvbHXk zXM%appE`SnHG0mAx$%NdGjsL=jven0@T$(kudBr~IX*Hb5M#3ZT<(+ziE%Zwa1X$C zqTGJB6NzI}2~Bs@8|72UV>OSOKSujqEByFNu<%L$?syUIatMk~Ki9;t`Q@gBejr7J zo-*m#jNiw6kt!XnKcS7(UQZt)oD{px06Sy1#6pW>g)&Vtg_Be|dg(DLns6Q%8jrVj zh`z{hk|sth2I)ssH7<gD&bbUiL?@}V0J!tdp*ii2cJEu#HN!C?pQ)tKJ*rJgdjsr% zN8XMnNF$%D)~gTc=Wo;{6?rzfbvFR{ve%BQ!=Zet9^K+kJXAy0p%Yy<CEL33&IG6P z@AGkwl@o?u*4c~xg!;fDKs#2ohr|}jQuiBg5Dz3o5}{Yhi(oWeJ}5ic&Af#gbvnuX zgnm0*3tqET>P1+6X@gD|>d`DaTCJA9SQ^z23CBbOG(7HX^E0cLL-Xw`$%%HFdT;Ez zMgKH+B-ni#vx6BH)(Gv@_3J{j-|zeWZi7b9_p9KD(1-@K?1d0@v(*{Ls$1v=wTVkU z-sJGv!0O}!eruk05AG#WzAaAI@G0l+56AuA>Bb}z&w~|)=SC?E7_Jx0>F+Pckiwwk zm^QLz&Rfr&Huj%e{<sji51+`B5N}?$w&j~_<gCgkF%9N~Kb;RQK1r9JwShU6E*;{E zQ;~+Add4O^xmNiY5eaTiy{Wpu{5z35P8D<T{`NW@aVzO%E79z01&jF0A3PN9f0vFi za=ZR0R>?oTYDDHU>KOJW)@jX^aKbmrctotJBnic*BvbbRyftwmHRg$HO=&hVupfp+ zz^uSJ2(_yd5A!@C+)4e2^z4rr$Mbf-Tx3KXFZvoyq_xDvmw8>IV6_qhy{8SKs#1_J zh8gA9qdRg$0O!(h0B*o5$A)EJ=mQtZ&<&1b{$ATolgN75(vH;#8AHN^9pMTJ7~{wi zqpG#wNA$0jAMjt+z-#4y!o>`1hc8R6FF_JL13xiGdtI|BJ3}5d(OG7tvfLAI!!eJ_ z6Bqyl&Z?F?WzFP8^%&9^bxyffoI|1~tb$))lOGXc2EEUimMYK)_bEp90`8zWhk*5J zQ@w-}O0|^yRPl0d^WA#*fn8xNHOD=nPpi02P192@W$g?J0}qyUckkt3&z^u65+}kr zu{K)4kMv62l`*|$yWSzcxpXQ0#1XV=<El|_vIBkK{JO3?0)3-9Ms+FIOVHq;3F2?P znc`fkS#a_gUMdI)yX7y;?rjaBu1bykiOC2n&I=TCvQ=vAV3u*O924?HpAZMQ<N>xq zJPRgzu)nq~;CN2gPI=tjuIs}~T#GJT;HOfBwr$G;s3)l=H*|~;b+10_Aren~yl~9N z0CovzK2sJ}H{ClU4g|tfJsw3LzvWjqR<JnhbHP5-QD%s%mAlUvf#v*eockw~8`~KB zdnTv9h3sByC=(9<l-;5~Q1mG_+%o1vreAHw(|kevdQ{!uC)Bl26A+C>>IkY2u)$mR zDc0%m)BV(xyPvpw^hPG%pu(SB>K|HS?33Py9&1B~C;KgyUC8@l@lLHP+$pRWQ<hyr zXV0GNx2&tDt5>-$-7)?@TZg8|@^IbeWSZ+wuJFv*x4U?ER`NC453fmmE|>OvPqBWp zG&9(fq;DInY#7WrXxMM{^W}#dUT=NP*=AXsne$vig@3F$5^%NGA1Q60oHFRoeQ@Vk z{pq=Ky1LuClD@Ww?N-7L_}X*RP|)Sp8)-0-?KPCXj6K9y4s(grBJCANZm@hd7Rb2X zJjcH(PU<~s;Nul_IPS^RC(vv`;btPcw~rk^C(X|=2?*rzZBA~EMxSCKo_o+5dU7}7 z9robew@8YZ$lV{L$jCS2E&2I@J#qNzj6LRBv``Q4folSIHki-j;`K6KeiDD7MPZRi zeRh76I|YBCn@0z}B0eHtFO@lK9@WZNZw;Gt`C`N1ffnukl2aBSvly-L{cEG^di-@P zU)Da%y1E+wJ7<hGzBkF75nQ)47R?PvSYl{Ff_8?K3f=}H86TaKnmcuT9j{37S3K>V zKIkBI7}BjbGqdZSMl<B=Q#LQS-R+)r4c<KHoHj|cpA4KobDu>?lnHbRbcgYrHoK6t zesMi&by47UQOnDY>PXoy$Z#RmA25kw(n$53q`8tE2l_rx_`|LA)iPDjU#}|dlypNK zX<<aPN)|k#o~z$%U>srW0miTLRHyFpc3ghJWO*Aly<PvZCbZMh4Yho1*aX#pY=_+E zs^^#lZ4iwfrvLVn{v(>Xi{j0ly&7uYL)URo^4pZgKPdAXpQ~6M*KHiX+HcPNFcYdA z<S!nneD9ca{3{*mdUYv_AE8u<gz!voF(lz9U%8aTgOAtwyh*8k+jn;%bKia9mo(q` zF7!Y@E(CP1=Et@8g#BRPda_@r@w&lkIu!gkPEBt+Xj2_V0yk&p!sht4o!I<|D17Oq zMIFu0Qo*nKV@LcoNo&m+u^on;zF_%XO53V8(zS?&0*m$6a9*@!XV~AA#_NAt?%wsm zBk;TnPz}L2qarL4Nub)?_vV>_-`2m!^KW`W+usamJFv>l3I`}W6E{(j`9be`=yd#X z0(VX+Z-3`cyaV%BUav0VLi^c~_@nyRoa&y#rw6L=FK+cS@E?5gpE&cU1D~$_AqAgd z3wv-pIy-9RO+G&{^%LNQr#%`YzP<BAOF!v_=3n%P<Y&2@86ye&7^AUnYQwKuvtfaR zcCX&s#)`xq5ET6Uo)AAu6B>g!xCJ+W>kat?lIhj3c`V)<33#zC22YAfS^n!sjwGIM zdOK4^iN8~QWaSgsulO#Nv1326OceN9(f9_Ql}a4C%NhsLM|{wtX04@8>rvL^J|2!* zRI|lwPh&7aJ#lM_d(Pw=!e1J*Md#^x8G2!Wm149?VUt@1JWF{=dP#ffFVhhv#z`4S zZb)v(2n_uO?!9Dm59dT!#Xe|^$2(`N$}lELj3)0WKUlZQb_uOhbOX+l(MQk6{v^bI z+Zd(VoxAdE?f+CCqD{&G#L1?ZOtL<7plRSW_3Rt1^84;sIe~8>SGR8YnZ1!=1jjbI zVQ?|YR>W@3j(C{BqI19OM<wq8lAfj^D<UP(6i!R{`zJ>BWFk{^&rm{&{AB;6`gdiS zL(`n|Vu-$ZUE%_wdQd8edLcEA^P#yn=H+F`97mt@4~$`tj2U#Mh>hdxbIYQ;>o^u^ z5Xpm6X!R(*B%dWVNh(NBNqQH#7oitV6)_b%6xRa-+7=0~m1Jec_kpFR2@`c<#I*6Z zhsIeQx_;t*N`7*FGW_%;sqtfWS2PbXtM-UBA)4c9meFDdq&f%2IvjeOU*>$<nBC^Y zLZ@Mc^a`9jy6>7i6af737YrJ494T2CYY6_m$Lb++I|S#JnrJ_~NP61+k@EP6t}!~_ zdh_I+ETY{(gAH|t3*vaV<&Au$CD4}FxdU!VmZy3iZ<F``%)AiMTlo{&=-W9ct+RXB zA)R^P8^QNA#SLrY>(IT?erm3=ISu!av{5=P`2PEQ$M?W5(Aq9KF52~CzTCpj(FQi8 z+l%TvJr=(40%b97KD(FR7-@tg=oePKrO<KRVB5*sV$DChm*WaeK6P+3D^xeb!Ew1< zZU@Qp@$%N|8lB@ef(=~;4CR>o@C7bq9s<I+(KdX2b!X?&2RDQ%+-;w9p6w8heST>> zbrNmm$!s)E!#S-<-_3AfkdzD`{)ljbKUiYSV?{D&WjsBt%#UFnlhAN)pESDGvg7DF zvImRdf*OZ~5DT{Yu`e*H0#b|c&jOjY)HRW9HWe=LY|T~*-|z?N7((X+`S%mmVA_lD z+{3)UW^X#(6U+C1SbV#(1$7cmWdt`K;;Q(@AqZYSoMY3D6TzW(W0Nie_Pn3xg7G9^ z$x`Kbi<vM8y#T8#kX;kir2uKKkGCxBq2`0N;Ya!a?XqAoMfB(`{CoVgp2nJYUxNuY zy)457K6f&T5OgATg`RJ62_uqWDPe+U1bM(1443_ycu@Lp$rBDnP(ymO?h{3msOwNn z3T5dBq-_V}jU$5xW~8;nwx!<whLMmYbxI<i&0dwYH&VdDm7*B?CPyA@nnXVf&mW9k z#B?a3o}fk^zLWIk;4an_@QutqNdajDU>TBIl<+Pwbw`~g<z2FoDdz_v@jfn2bcSi9 zsbr+>kM$$A_k>-BJet@%1M5cZOYLA-Jt>Fc$^$zv=qYPmjAL40x?KvzJC6^Zq_6Y> z{T?r_&*`3r(4<|Gp`7gIgr+Wne(3&%)-(2Gc%&&sS7x0YerfK6$rOF)V7S;n+59kP z{_AXouo_U=zbOa*8~TpKtPp4i1$I`RCbcZ>QHDlUBG?k4JrXXteus*e<n1>u>9Q=A zicfxQ0c)~1^8r-Dp0{tk6FWmL{h7SQ20FXz;}>mg1$g@SO{;%qUch-LKL`89SoE)e z;oD)!cHvK$^DxU#&|)&&w-KEaIw^{!4sG!W7^RY;VhzcbsNGVF(%5X-2>OTWoNRfs zdnoIqlGLwED5_HA%r)w1s${Ry-!7`M$I4mk+QMbiz)Ewg_ww4}WHTHV1Ju7&Ow;Wd z6SCJ?v!g$M7Dq}GUVPh>uu#X{l%dz3R#Tj$BHhR7N5mtoZ=7Faa>ALXW>G}4i1@uv zo1z*?Ra9)L^n~t;^eLtTYO1Hc2HmZ9?t*p|#oH*=8o5I<JR#jmfD_UBsJdmoxH}(5 zf6wJA&$peR)KNV_5Z0P^tZ_5k!V?N+`$I`MKo&Lz1LY$Ik`V?PC9?>OsPH?{pNP27 zEP2lAHGxFNy>FZ_&=C_5<RydF6HsV2gH$Dh!6QBN<j9YL1_$s<(<IIST(};=5onGb zkvAwo!qh>%%rIfXK$PGu=C|T~)GbKj{djoDKbc)Jp|FjxQUqya!zzuCQhHT0;N}Vm zclVtN-kS8#|GJg_b;@<Hws5d<J=2U^;O?^VRpNZFs}tt=QTx})@l11ip@B<v*9S7R zA+i|JO6K*dA-o1S`{9H7G;?DgEp*7wtVih8*cofG)8Mvl^s~}ups`&LJkam=2eMg_ zn$*2MGRR*&QBEgLi(x=}9`Q1wRe?gpJgoMSyZR4q{Vo^09Z*btG8Zf-f!*7aT|rO3 zX*<G{PF-W{91KNONS9VfqdLQ9Vuq{x9bx#c&+$IK-}ns^SLp9YOCP8=grymucCl`D zvK2+~TS4>f@2_cwEyjr5!4y8%0;Y%tMz=FzT0_l{HH<-R$R8P#x90J@Jnytt9pVuk zwPX+;n6&#+c?&IMED!5KrrNdoKI^4TFPkA6=o%xQS|YATGX##e4`8i}$Q+Mje=l5P zbFB|S;|$1lPNdjW87>BH`8D9!BUXQ=piKi9M859(yW5d_Kra&UkB+f$It50g>Pr#@ z#{2TeYF?fMF4ME~Bw&fIXVie0k04qy*^bDS`xWK`{6hxh%PqlVhw~xDM_fJuXI7e@ z${>O1o5qgK73xD;L*n^})Rn>&lXKMX?3}$c!LIWL7-#WXm@8<m=(G#6hw?eB5w1na z$z-=#p^kUdg()nwO7GQFCZ-qyt;<PnO5d>6%wa9E-@<73RjNwdutngHe6%j=WeW+L zWyY76o0lcCB#-?x(|{XW$kM1t*Qmr3Ta1P;@zd(9T#<a1sP~*{maraMYRL?^q0GQ5 z|KiWmVb8`zV0c+-h9H}Vvc_lS<;!_Jz<MdHFy9O&A0laQO-cxO2nI`DR$3NNm=3w3 zH13Q2Jwf7xAQM5x5uM@q={T3f|IhHo&CPC_;A3JAa+;vC)SDAP@JJ6NOW&{^pu(dv z6vsDUjP?6Wgz5YVB0T#nS>R0i_Ag*Fsjot$Oj!s?HIiwHVq_`iF4&&ZR)x)bElk<9 z!hmm9FP-8v+b66OO}xY;#Kbe?wu}lt_KJ%n=;PvTo|i6^sbPJF@xon#S7>G%l5<Ge zJ`oA0y!Q6yQcrQO%viVVt?0$QX45V~W*-~OvERvYMJ(0X0=qk51ykTUCwuEeoumyl zBb}z1vv|rP&bq%_mSKps3nHdDa1HwY(XP=LUM8`%ZhBU-L?;R#p6RPCb(Vfa|Eb&0 zk|(OWp^GS-cRld-Aws(xs#%y$ai4}Hy^WMiLOK;2fD%fcVm}Xn6k?%D>NZx+=i)4u zCBEWhi^%xFNj__y0?2i7485(%-WpC1MskLsoyKzY1m?2)st+ki;_CX#lBL&NF;cOV z-bSAfm({OBSL7=WZtY`Z=YcJ&bIf>Z_2y;HJh>aG8*%Ra!&;7-ZFLPriB<G!AsBUZ zl^2WA9uvQ~(!>S413T-CCc*PuF$`8wU<sV1F=QD6Be0Z82#@e8`Q5smewv9`@rnGT z!sI^Zu(f{4CX>C=(Z&ik;Qp3Kg4f43T^}L;n|_QjyS90h)CskXfZye>;IPc+1XO=% z#IJP~GY-?g;6(gS7RsFQPSV28-*Aks-<}SFcDwK!`!Dz$lN>D4#SGiy?ZfWZ`?Gr; zz~AJBzo)TdpO2bTpEMGXVP&(w&&F=yQhjgi&>?ja`(B<-d2&6im-@!A%OMR;M#M=h z*sP`)i$m8LAqF)sOc|HaLRad&{&2b`!CJABUQua7gFsyJz;NOKCu%WgAK?aq%K!qW zAP>vU86TnAJB~I}S*f2_eP!bAMl=;LrxsHONke>PgR{yz(q~EUE>YFY^T%nZUFxtA z(^*D238BNO?JO0~PQDNEp`t3GuA-+rnZHd}Z}ELU0e}-!v7#K4h%VW6vw2rtqjReM zuCf86R9fu%Xm#CO^2rRf)cuDeoOCuLTh0TI73XoYWy_Sqtm8%=XcgV;Yz&n1yivOK zf@{UOpAO1d(d&m+T-Ghz3hYh}yY`C?TkDqUf8euefij!i$0z;;QSGzGhvOA=Nt#Z= z+)mEZFV!zCG@1a*&0%mgWU>R0tV${|VL9ejFg|1-(u#(ed(nL=FmJ<3?8^KqSY%Vu z3_!9i;UGh@EaCWg<xnz)`2d<Dz_iLfWD(6W_kt#MW$h$RtjN?4m$WYVL25h!rXgLH zO$U&U5^d)4*hctNWpwDt(#snk%6v~va;@E&^uf)I`;hgf8jc&9wE_N6w&QLVsUz>* z4|ka1mHFTw=~i6&?h%!Jo~13{r?osfkq#>lYzf71R#|e_L>tb2LyhaAZO<jdonZg4 z+pcg5XPhU}E%UZ7JWC91+eLQeLU#4VIl6lO)}E1lI3n+K6Je9XQ&4?2)73WU>w4Rj zvx_X-2{z4W5s$9?Mx<R~L$mPQsCs8uZtqoYZ(PpUq?%45kI#%bpQtl%WA4<cu1h!Q zTGhfmxNyKudB86_Z6`gwmS2#aSa6(P;GG9!)K*0HV*kvz7OB^Euh*8L*QTe`wm(9E zA^J;BXtO!HL#PHsY2CFd>t~x~EDqKdD$OeSB@C{!ih`bTms8o^`HgVJB|G!UPm)fy zw@R8azt=o+gM`9`W|$<ob^C?P(YT2!89h5^%H)m=iMy&mL~in1#JZI;CBUolo0Z6~ zk|2Et=OKVsWBFpxLABhVQ^YOX`%-{cYx!d6!9&URhls4~C>;#G%7W6Yq)JQ>h+>1h zTM++arhWfmI)_|j&oV)Fi6~iHp_Md==noFLL#lnMWE;c!gxdAyFz$|S^h@CZA3J2j zFP7DTjh}q^GwIIuKkl906Vif}VC`od*uQ4}%#@on;+&YLv~UnDmK(H+kj{!49(~|u zc@s5g__I(`k`paTtaL{1+e*S|Mad3Mihq&93#QyRhi?yl8Ix@Y=V~Zuy?hPdY7txK zeA>6KPQ~jSV5O+Jh9fs|+Og2(f#uK?J3ek^B(`#ss1a3ilOH4K<t9Z%J5x}Z>9Wur zX!mRox0Zba@c>%C&>h3Ke+$SZ?vj)aj9PFsj|c#A)+b^Mq+ZhNGdWi&AMz)09(XL; zR`+C+yImj0zI=0g7+KzLCBNWz*6iE4%N2j<+%|k*kcjR;KRr#Q$=>0c{BSj93E)|s zz;c5BwvwB9g*_N})wFH>P;0D@ODYiHdq%eo`19szZL2DcfKd8*;!N^^k}b)`F=u(l z(o@Kpwe|c%D<zjq-o9|1RNk@RR$k&hC;LchoH;`jkBc2E#TnFtb&8p@lY^%)u=_3@ zWL-L5mONa}O;NKz3$!i7TEwANsuYhjQb~0LsYN=>r6{uBlz+U!z(u=@;VoOFMDNLL z0)1=MU8Lk{*ObjqTpdqMYs;i#kU7iE0gMg4T!rLjNoBoc(aV)kieZ=5T6<Z20u?SB zei5qt0cZW(sQpEo&Z-=zp>SgT=b7e92Yv-kH>`@xZ`zs3^bhwa5|L?rH9o9%Pz;@@ zTk0Y25je=`B{ahv`T80WWH4~Z*gWC|2EETqP=7t@!G(6@=^s~iq^%!)wl9|Pna66= zRGf%-$cb=AgZ2x8{97@;6@5MK*$W-J6pZZMB73Ko%=kNl=|>^ex;^>ff(Lq@TWtIt zt)1+CNVCS_#gOPrL=8bcA8Ry{`y73ow2WJ__HwBueKl2MXz>WUa80jyR;Vp=D4D4V z`{MYLA!|Ki(?X8j{_=!aC=Z7N4t<5U(HBFl$~KJ}TNQEVZ5Ym^Y%)`or(l&Du7#Hz z)L7w*#MaHB-c|eF_n4x4&WtP^q>?B&jC(Ge3B`-OT5gnF2}Y%4Qf$Kpmhw$ed#%#f zx@Kt3g=@v&X4+GATZKn~!(o++{L{<%te=(5R}vkEx1{KOzlK23mo#n<DA8v7M?bqP zx?2ue&DEWC&#ZXu@7}S0ZNk-d`?|C&a;?7JF{Iyu-Gmd%wA8wAN@J`2Q=cpkB_F@h zY~V3^PkRmYBZ7esr70AhJ!lGK?TbEf?0HSi|HJFA5A_{2P(x{<C8*6_Ao}R|{U;01 zd|`DZ?MHkeaprm=48%>opq_4V-WlN1%GWIT?{OTRB(-edIaAmRD(t-;B*|d+fL<ZR z-u`JA*dm1EJ0XNx<1y6?ap%H_lmIk|T*N=*$%Z(#;mI{&kNv#7(9wVoERG$P8BkBX zNNVe7)M10uP&Qjiwgk{No#!6$KW(izbx>E4+>`K%n)~fJr7Jq0E_JB`9%@kI5(3vH z5~pHVQInDsqX?-(jF_<`@=)dq-b(B^By3Q~bT)h4{X_zB8qk^+>c;o5wgkD52h*Fu zp77h!^js192>vSS;hB6&xr3TahFaoOr&*0NBU(k)+0wEMM%yMe8F4$;b@+0p<+!Pv z>WIAxN3f-Ef%IE4Ns>jb6a<DJ#X)7H_c@8Ao`9yF;=hl$PZngsWDPAg#eE1&tF!1p z?#O0F-~kX>timz%>2pGz_i2nADtwZ9V~+iF=!VnBUL#jASV7h4cAieX0l-6cEh}88 zfqoDO`Rw@CFQC2!jx~~;Me4o5C@NKQEw8YfqPWJa#DUxLP{WDBi5gAK&yYLp^AGdV zgye;B^*#>p5)mB!a|JkK;`0m97aFUT&}Fsgn<n&ME{GB(5fFznKRFW9MN&kXBBX7# ze8p0hX)@#z@7g9|Q<iqGHEz;>YgJ6E(l~E(ZSM7C{6O$Euy9YVteE~XF6n&r-Ml%) z_+X|(VCpu}mbM3myAy6l6jn}%e=iN^g#Voko)RXiw4osCl2nj~z#_bi!8YX}HR7M| z4k(#|l!c<n-`JFe(voj>7_aEl(ns{`@_od+NJ~e!J{Cv8ZGL?}2Luy#2HFqIif&k3 zo}|1ZqmZ&cBr6wtUw%}XRn?}`D0f=w+4j5=cS_XWk>noY{>44{&iDLBPdDvIfYJdh zt0O9?CCp0`&Aktfo}iXDqU|@bHj6(;EBu{@KpMy3Ir;rLQQX#9qv6(uQPURw)b`_V zrD5qU9()=%wrmvg(3UnQP3aN(hFI7gd}nB!@%@qDvWG7_N)1(dEkkF!cFu%j``Zl9 zR?cWYh@|y2#Unrlat=jfwN*uT1);X3)y5Bjp|xQ4K)s2yI`5^=C%{|EC;U67w0VGC zl77;<TA;K#s@jAqgW4D6wd&<U?lLcfn^bP~PCb`W^WZ(Y)zruqj-~VtXmlau6%-~x z3O!kOBhiRAh`zq?dy4`}3}27%v4Xri-!<+FS2K}~T^ur`z6Aan;`5RO<=Ba4w-A_! z8T?xOe4sN}L(I32fI|6nAbOAGWkimA*%Y|+)L5af7N5;S==Cw9)*^ywvhkzOno>Y5 z#tOIoFJf~b_0j|$NzNzg%d_sv4(Rg)qjb7?z-Ti4nT6R#C@F=cx2F=HQ?V|M#%QYZ z-={w=V!S_sw5u!LDR@7eS*B{2z>vp|_ZU!oi<yxWXcVW!D7>*Lp;6GXh-aswh~>`Z zspE4`*^)hz6!B)<*=^c)mXw)b){ebIKwHpwn&PXf_<gwkeDe9)i8DU+&r~vJJZ!#s zbNiUIh&rspJh1UhoyhDNFp5P$cN2XT-6(Mron&%KPdiIGNChjAA?G42DLE2gSkFaD zO1nt--gq`VL_!>fN=+O`ENpnV{dbudd$>eq!!3=MN*$|p{Xmx0po&Ox_%AmG{+4^T zQ8UipP6RuRyL)?kjUR&Raofl3mRik=5QH7wnt<M^qp2y%X@%clt9t#H_Q)lrrDX=; zbLzUFrP6?`{EYl#S(DvVGi4z1u#CQ{0eciw2^8PDnk9Rc@DE>7$&ySe4#Jx4d0p1# zX|Do+jBTH*OqBBr7f#6z0#Co6#G0_OsD(D#-hgV&yT^;9@}JEfr>Ae%_5fMp>F)EY z=;<tnl`ePsDh<>#3op<ZFYeQ{0`u93#LlJZWfiK?A{=4TmHk=8KZ@h~$gNod^=;;D zg6IN|pg(~FmxmCHBk_oUe8w&z?`C8z+G6c-%zQJkaC3(Yd=(?v=d9f`C@=I*&DYw8 z(t{)EJhUC<F5(Pn@O0c-`#)EetbyS&^eo^+n=;9!(o=Vv8k=`X*(J~{quKIl==$aU z06D7peq>DC?Dln1nJMBJqt*@=Rwx2L7=5q$#jhm}IvQJ-@JiqNrOkI0udy~w+`Loa zE$)EuO-apdoR^zciu-P;&nPpqA6L@1sA%TTrh$=}mqHEG(i;Sf4YAI%uS}DbqwP#5 zwSn=w<$b3{mow1Vu9vs>i!Jv9fsvm5-;RDgsWc}JtB{b~d#u}Ef%{y_r%-(u9Sr)i z%g-q?u0iN6Qnyf2Xy0YN1Ndgp=ip)Bc?h!twi~M6ym@$FFZ~!>RE+9u4!@K!w@E&h zdz>@BBs;eDD5TxYv#kEpZ^d_fVs!#MG${o8HP~VljR@30AfL`)>);t)?e~#`61T#% zN6006YDiJdfo4cGtrSn$c7xxymAg-ot22^}PGwQnvL$Bk9Zp5l%61*glc^)Y&m)=h zxgR0pZUhx7n=SOhFS({oaYULRz}L@l-M;u-pU*MAuq!$cW+t+pB{L{6R&X3biwN(o zZl}al=I#X`x?EomZHT!sMIpwKNfk;BBE!EYT4uRDtE8eB4TKZu1kTJ0I5_rhhY5vF zf${pxx~GeJXL_}OT-YaDF1RfIzjRiS%P~$B;{8^3h|_ExDQ20~G$l0;BwO$)t)*SD zQHvvsC$*L<dbGkS259#iBeOT-Bhgp#8c#Vo%z2ETR&QOm(VJW%mBJ*k?BgSK?c<xq zfNAER+=rKTEnoQ3Owr?G(UK#izo2u72YGcUeJ06IFhs90z&(qJ8v5cvyR?xzaFf4E zaI+DXSlapi#!k>~--#l-g(<rZ^*mtfsp*<LGUWjrhcj|B-e`|B6=3+)09(C~1NnRb zTP{tZ6YRLKV|2aUFHEFHRBmAVAsIJ6nJ4u`p6IveVBJeOne^F%>8vp;*L$GJ&kO-u z{LVaiQcT3Xz@4l_G@9pCdiShlGqz=o-COCyd&!3^X4q2?%4Mgbd+rN-cYTktV9p6_ z!?&3u(yqmONfDzF`@zsbB0(a*l)iuTdwF@xz=K~nh?aZ99y@_^h`X?7fsFK#M_4{= z8UwsnYVw8M-<|x2XcZA?X5E-mrRf5kZS3zEb=k_iXM{o)2Ca*jNKVE_h)YClJ}4vZ zwISah1>#ml$SR;D-(8hgP_eGkpYszpnONhTC)(HmorVF`2zVxERkOIP!Es>tH5pZx z5;U!h_r7a8!%3f4JBjfy{tzjt33Q$%!hfT^jHMXzD989Q*6ruh>uVPgdeG-ei8H)S z_V!c%XSv9&$P%f?*$d@4dS6$M#zW#~lKwxIUZlSf=a9FUGSEUum@}dKq2=ZaQu{4^ zTKHz)!B;U2knapg-{RTDty#t+9-B0;kjB1X&5>)GlHwk-X}b#hcx3p0q6E%Fz=zKn ziUxa9ew#9Pzp6KUoFp$j-h9wiRfDG;Dy8wm+i6VwAh#qXB^BoTbgz^UD=va#jHe>Z z-<((zsOxPcT`+Z(3!`fxjaqv<CQS>Vn`M{Rs$E}b7CFl<;6VRI3!?gUicna>7Hh+t z%gd~m-DRZtV7I~@BZipO&=Alfw38ov>Phx<MS%DnZbBG7nc+>-tQ@UCL?BW#A%<LE zSjY$H)d74c<OV-$_l5;cLQF%aaP9su7`9{t_W~W0g`=EKsBB}mxkEtvq(m37Cx!+d z*CvCD>DYGsr5g#ccf=Li)pGEjBxa^Z8QQm_x%6}t6LCCZlalCdxwIMl)P!AHg2abW zy0+<*5Pl`A+CvHsv{RLDl%^7kvn&=2M;@lmPn}YoN%eCRKkG+7v1rVBr=Ht>!D9;h z8Fimt)4mpLhiMvOACk9s<a{%)@HsYOT2yrB7ykE3Rh8mah3kwxt_jwy>QEB2KfJsn z1sns)*=rlbpZzE8YljDJYs2^OPCfz)N!2q}{r27_587VbsSF+oohoChIe{B!dJ3I5 zQR<fV&i7GRHou=oQOQS-bMW&|x?*5M5i>aJaC7(-6@Ec!CJ7eX8Fmnja=hfr>Io3p zn=DXL7k`&}5wK=bhS7+2rKq?w!lt2WJS6GtaI~3jK1nA}<bN}6H>q1;%(EN~e><8G zi`X!d+dI2rMhpmwZBp}0q#1jWReL~e`z5(cXNC<It+0qT((CK1PJm^2S%0e{lO$G` z^dk2CV^d@xwrlcFDk@bN^#k=~;%E>0E5m_aNs)*d9DHu_MdL<1J6R&*Mr~X6&spVj ztaEydu?yzovdu;9Y?+xNK_66fM1+Oy+}oSl%Ems}vM$dis<<>?8j*pxgF%e36sjgD zwRN)Ta=F9@=xq^}dnDNJKj0NmE+L}90iC#>e?F(WHIhjfX-L3|4x%EAd`LLv_P9^j zy7)*O!Ug4C<EFxBy&!S9toE2v#1hQTp^^ManwFJ7eRt0|mH%4-UlDiF%-Nh_w)4b- zC8{?Ht?g2&Db7LCZxX4Gt8_OWhGE&$MDM9~x!_ciSk6B4YKidgmJPNY#B6U}(c28i zr*XoEe%z}F(K@txmXy*Jjk?iG!pAT>LXeV1M~*={vKVbf867$B7$}zO=00PmT1qfi zFD`w03e6xM*X5rlG$}Yd#Q!rr5EmC``gvw*_fwdpNzOMbd=rv%UeG9(wBD%qFH`H| zM`B<79~<}>id#gNzR8S2c0V=%nufLUX+0!omov1}edZ)zh8`t+8{m`+qzV!LFswL{ z$N?}WA1#smM6YI0P(-OL`-Z7HeH!4BBs&Vq%SettgQnwUG1>l5@<;scm9Qz`!2A=< z`(2vzWrbfG+Bf2=`?}17Z{f)jwMZqoU_rC`^B_O|a3MDA7tG%rPnJ39Uojifreq8- z8}bjH@(Nrf;b%>#krT-oLC@mT)<p}yiV8rIHivgXx7gtJ0_X%Z3tA!gD=!=hOX%6& zaYoC}Wm8ySW|Cx73~)pNJR-O1^k5L0g!a@|EHKP$V3q82Cyc`T(neu<_5MRa|6YT; z3<?Gw9uODLN>aC>P~$L5nfZE<*iD|c%;oP)8l3fIrU{l$)_!u&v#`W6FBmcw;cR0b z`mA3|yp(*^CVsae6Zw2eB_7+%dbmjo5^K!668q%ov%ssMCnV<@3ZTy!tt~PuDN--; zWO1QCEVp`2QejP+d(^c&w|x)#hWn_#pf$?Sn75?nN?ldjr);33zm=(>&+|i>wX(hA zVPQdcI%}zM7n!!wTpP}N^jN8YTYp0;zAWntM$5e?=$b+AZd6soddxSa^y_+^W!G@f zHNEamt!kB?qDnJPZRt}C(y^g}uWxD54UWQs4_ZAR^`7mNPgSm}{&A-Nmr`r<VhesW z#iwgb#qBn3!ea5(PHc-k?5V1%ic5x$rjD*5Wu=?zuhD#ZE;_uNFSha3!NV##M&<PR z23zG{v-gF}7#Q<6KA#}XDB1|U;kLNm^jR!)o9PH!ZY3V8dh){XO0jUWQ}gvKZByKH z{s{I=A9=r<K9lZX!sj!Itl~A0lIO{ENpoCTL1!S|dBfIIaHV~ss=uZ3WBTf7u%7#e zlAfAYSIh6m*gK-2-pxQHr!T|GP$0$FyU#|61r;!=!d!5@&UisawS-4)j=9wua!14E z`zqa1d>4HWdL$&Pu-IaqEPK=)lj1M5lT@KH&*aT)g7}PPbf-TYI;>a4cy}*I(5<1m z{+pN1^*5>dAJpwv^0=s(i;1(9BV?fAA7pZQBRdFVUDe9Y%!NVN!PXQawKcMU5VVn) zgj`G@+;9K~2tp2jHG5sKye?G0W_IcTAUh``7aNEb!T}dCa+EZ)vakdL*f~Iq9GpNF z5E7FVgz#-;B4lr2YX*QAy>2f27e}3eg@uC=#LC6Y4q#wo=3r!DVF$4TfE*m0jGWAz zkSmr~0y_(klM_N)e>H<}z|B|yuN?7L4*Wlf>A+X2`~PC5zpDPj;O`&-uOL9!?!rbc zX0P}Ecc?0^#^Apg`yx`JuXkTzVu6GsC93KmAtfqj<Ot#9n?mBY0(<@?R`GO!#4crT z?(j;&S2nY-f{X%q0;q)`Llb6@2@3^hQ!{5PdkX;dzsw*)71!4>5W83Sm|xlY8m}~c z9S#l-Mov~1$ehP(AP`*j03cR~n1zKA$j-s_N}y+FXJ_Ps0KugPVCMwrfS6x3A^d(2 zJ0lRp!U`DyU}I(EWaD6G*8^|@AqxMx0a<bYbXZv-<0Q<GZ2S+4zwDWr^&rTKYW(HS zzu^2I6EFW4=bnY*e=+X$5-03K0PjNYcn9_5ak1xnop6SVa+@NfUy9s_p|-50Xt4%D z9SmS>l9ME_JkPms(SB(QeAnhGICp!8%6*WMhEwX)^tNzN-zv!*km^Vfs)w?QaWpu= z!bNDk7F$IB<{-RUev;9#CEX<2J)}!3QvHw^t=Z;hYi8`j(3{5K5Y_jRCmnOh9MwL8 z1ww9B#np7pgq?fUKV5(bTAOTn;xSlM{jl0!y~)kCHDGt^4c-ee{8^Y%T5#KrUYYj2 zT9ZnmSdcE|Rm>BkC9M^Vf#og@ykeVGDGC=Hq!?dat>4gkn$cu4O)rb>dSIt`p<gt| z6gx)!UUcU%VEaXZl2;<a6jj7()nGivT<yySI>0DS?!P<nbyVa3Q>y<izt_5hw8;Oz zNX6_S(=@L&_3u(-ko;Feni@g615&t<35>t`K^c-)RR=YDtJeV>0Pr=Z|D$C*oXyOU zm?8az#Qg6Azzzbjf&k`#f6IW70g=}OVE=C!hy(JZ|B$i#mkh|t#{A#5tjv(c{SR9X zF37ydf5;$p{NFMz2y*{R56HsC1!?L3ka4j5r++|JX2?6`-+EwYBgi|%`E{I2#mWnU q45Z7I9ULGpw^xK9@2c01{uf#<U?XSnUswTIL2S%O6cl0#;{Oly6>4Ar literal 0 HcmV?d00001 diff --git a/test/assets/text-order-detection.pdf b/test/assets/text-order-detection.pdf new file mode 100644 index 0000000000000000000000000000000000000000..840ba9f86c5514770e38ab6da15b013ae10704e9 GIT binary patch literal 61990 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 literal 0 HcmV?d00001 diff --git a/test/assets/text-order-detection.pdf.json b/test/assets/text-order-detection.pdf.json new file mode 100644 index 00000000..fce9a5bf --- /dev/null +++ b/test/assets/text-order-detection.pdf.json @@ -0,0 +1,1309 @@ +[ + { + "number": 1, + "pages": 1, + "height": 1262, + "width": 892, + "fonts": [ + { "fontspec": "0", "size": "15", "family": "Times", "color": "#161413" }, + { "fontspec": "1", "size": "16", "family": "Times", "color": "#161413" } + ], + "text": [ + { "top": 53, "left": 60, "width": 7, "height": 23, "font": 0, "data": "3 " }, + { "top": 53, "left": 71, "width": 28, "height": 23, "font": 0, "data": "May. " }, + { "top": 53, "left": 102, "width": 73, "height": 23, "font": 0, "data": "Bistritz.--Left " }, + { "top": 53, "left": 179, "width": 45, "height": 23, "font": 0, "data": "Munich" }, + { "top": 70, "left": 60, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 70, "left": 74, "width": 26, "height": 23, "font": 0, "data": "8:35 " }, + { "top": 70, "left": 104, "width": 28, "height": 23, "font": 0, "data": "P.M., " }, + { "top": 70, "left": 135, "width": 15, "height": 23, "font": 0, "data": "on " }, + { "top": 70, "left": 154, "width": 5, "height": 23, "font": 0, "data": "1 " }, + { "top": 70, "left": 162, "width": 10, "height": 23, "font": 0, "data": "st " }, + { "top": 70, "left": 175, "width": 28, "height": 23, "font": 0, "data": "May," }, + { "top": 86, "left": 60, "width": 45, "height": 23, "font": 0, "data": "arriving " }, + { "top": 86, "left": 109, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 86, "left": 123, "width": 41, "height": 23, "font": 0, "data": "Vienna " }, + { "top": 86, "left": 168, "width": 29, "height": 23, "font": 0, "data": "early " }, + { "top": 86, "left": 200, "width": 25, "height": 23, "font": 0, "data": "next" }, + { "top": 102, "left": 60, "width": 54, "height": 23, "font": 0, "data": "morning; " }, + { "top": 102, "left": 117, "width": 41, "height": 23, "font": 0, "data": "should " }, + { "top": 102, "left": 162, "width": 29, "height": 23, "font": 0, "data": "have " }, + { "top": 102, "left": 194, "width": 41, "height": 23, "font": 0, "data": "arrived" }, + { "top": 118, "left": 60, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 118, "left": 74, "width": 28, "height": 23, "font": 0, "data": "6:46, " }, + { "top": 118, "left": 107, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 118, "left": 130, "width": 26, "height": 23, "font": 0, "data": "train " }, + { "top": 118, "left": 160, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 118, "left": 187, "width": 14, "height": 23, "font": 0, "data": "an " }, + { "top": 118, "left": 205, "width": 28, "height": 23, "font": 0, "data": "hour" }, + { "top": 134, "left": 60, "width": 24, "height": 23, "font": 0, "data": "late. " }, + { "top": 134, "left": 88, "width": 69, "height": 23, "font": 0, "data": "Buda-Pesth " }, + { "top": 134, "left": 161, "width": 38, "height": 23, "font": 0, "data": "seems " }, + { "top": 134, "left": 202, "width": 6, "height": 23, "font": 0, "data": "a" }, + { "top": 151, "left": 60, "width": 62, "height": 23, "font": 0, "data": "wonderful " }, + { "top": 151, "left": 125, "width": 35, "height": 23, "font": 0, "data": "place, " }, + { "top": 151, "left": 164, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 151, "left": 195, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 167, "left": 60, "width": 48, "height": 23, "font": 0, "data": "glimpse " }, + { "top": 167, "left": 111, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 167, "left": 151, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 167, "left": 158, "width": 20, "height": 23, "font": 0, "data": "got " }, + { "top": 167, "left": 181, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 167, "left": 196, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 167, "left": 206, "width": 27, "height": 23, "font": 0, "data": "from" }, + { "top": 183, "left": 60, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 183, "left": 83, "width": 26, "height": 23, "font": 0, "data": "train " }, + { "top": 183, "left": 113, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 183, "left": 139, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 183, "left": 161, "width": 25, "height": 23, "font": 0, "data": "little " }, + { "top": 183, "left": 190, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 183, "left": 197, "width": 34, "height": 23, "font": 0, "data": "could" }, + { "top": 199, "left": 60, "width": 28, "height": 23, "font": 0, "data": "walk " }, + { "top": 199, "left": 91, "width": 48, "height": 23, "font": 0, "data": "through " }, + { "top": 199, "left": 143, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 199, "left": 165, "width": 42, "height": 23, "font": 0, "data": "streets. " }, + { "top": 199, "left": 211, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 215, "left": 60, "width": 37, "height": 23, "font": 0, "data": "feared " }, + { "top": 215, "left": 101, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 215, "left": 116, "width": 16, "height": 23, "font": 0, "data": "go " }, + { "top": 215, "left": 135, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 215, "left": 164, "width": 15, "height": 23, "font": 0, "data": "far " }, + { "top": 215, "left": 182, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 215, "left": 213, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 232, "left": 60, "width": 43, "height": 23, "font": 0, "data": "station, " }, + { "top": 232, "left": 107, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 232, "left": 123, "width": 17, "height": 23, "font": 0, "data": "we " }, + { "top": 232, "left": 144, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 232, "left": 170, "width": 41, "height": 23, "font": 0, "data": "arrived " }, + { "top": 232, "left": 215, "width": 22, "height": 23, "font": 0, "data": "late" }, + { "top": 248, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 248, "left": 86, "width": 37, "height": 23, "font": 0, "data": "would " }, + { "top": 248, "left": 127, "width": 26, "height": 23, "font": 0, "data": "start " }, + { "top": 248, "left": 157, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 248, "left": 173, "width": 27, "height": 23, "font": 0, "data": "near " }, + { "top": 248, "left": 203, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 264, "left": 60, "width": 42, "height": 23, "font": 0, "data": "correct " }, + { "top": 264, "left": 105, "width": 26, "height": 23, "font": 0, "data": "time " }, + { "top": 264, "left": 135, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 264, "left": 151, "width": 53, "height": 23, "font": 0, "data": "possible." }, + { "top": 280, "left": 60, "width": 23, "height": 23, "font": 0, "data": "The " }, + { "top": 280, "left": 86, "width": 65, "height": 23, "font": 0, "data": "impression " }, + { "top": 280, "left": 155, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 280, "left": 162, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 280, "left": 188, "width": 24, "height": 23, "font": 0, "data": "was" }, + { "top": 296, "left": 60, "width": 23, "height": 23, "font": 0, "data": "that " }, + { "top": 296, "left": 87, "width": 17, "height": 23, "font": 0, "data": "we " }, + { "top": 296, "left": 107, "width": 29, "height": 23, "font": 0, "data": "were " }, + { "top": 296, "left": 140, "width": 43, "height": 23, "font": 0, "data": "leaving " }, + { "top": 296, "left": 187, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 313, "left": 60, "width": 30, "height": 23, "font": 0, "data": "West " }, + { "top": 313, "left": 93, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 313, "left": 120, "width": 50, "height": 23, "font": 0, "data": "entering " }, + { "top": 313, "left": 173, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 313, "left": 196, "width": 28, "height": 23, "font": 0, "data": "East;" }, + { "top": 329, "left": 60, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 329, "left": 83, "width": 29, "height": 23, "font": 0, "data": "most " }, + { "top": 329, "left": 115, "width": 47, "height": 23, "font": 0, "data": "western " }, + { "top": 329, "left": 166, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 329, "left": 181, "width": 52, "height": 23, "font": 0, "data": "splendid" }, + { "top": 345, "left": 60, "width": 46, "height": 23, "font": 0, "data": "bridges " }, + { "top": 345, "left": 109, "width": 26, "height": 23, "font": 0, "data": "over " }, + { "top": 345, "left": 139, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 345, "left": 161, "width": 50, "height": 23, "font": 0, "data": "Danube," }, + { "top": 361, "left": 60, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 361, "left": 100, "width": 9, "height": 23, "font": 0, "data": "is " }, + { "top": 361, "left": 112, "width": 27, "height": 23, "font": 0, "data": "here " }, + { "top": 361, "left": 142, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 361, "left": 157, "width": 34, "height": 23, "font": 0, "data": "noble " }, + { "top": 361, "left": 195, "width": 33, "height": 23, "font": 0, "data": "width" }, + { "top": 377, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 377, "left": 86, "width": 38, "height": 23, "font": 0, "data": "depth, " }, + { "top": 377, "left": 128, "width": 27, "height": 23, "font": 0, "data": "took " }, + { "top": 377, "left": 158, "width": 14, "height": 23, "font": 0, "data": "us " }, + { "top": 377, "left": 175, "width": 42, "height": 23, "font": 0, "data": "among" }, + { "top": 394, "left": 60, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 394, "left": 83, "width": 57, "height": 23, "font": 0, "data": "traditions " }, + { "top": 394, "left": 142, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 394, "left": 157, "width": 44, "height": 23, "font": 0, "data": "Turkish " }, + { "top": 394, "left": 205, "width": 25, "height": 23, "font": 0, "data": "rule." }, + { "top": 410, "left": 60, "width": 19, "height": 23, "font": 0, "data": "We " }, + { "top": 410, "left": 83, "width": 18, "height": 23, "font": 0, "data": "left " }, + { "top": 410, "left": 105, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 410, "left": 119, "width": 35, "height": 23, "font": 0, "data": "pretty " }, + { "top": 410, "left": 157, "width": 31, "height": 23, "font": 0, "data": "good " }, + { "top": 410, "left": 192, "width": 28, "height": 23, "font": 0, "data": "time," }, + { "top": 426, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 426, "left": 86, "width": 33, "height": 23, "font": 0, "data": "came " }, + { "top": 426, "left": 123, "width": 27, "height": 23, "font": 0, "data": "after " }, + { "top": 426, "left": 152, "width": 49, "height": 23, "font": 0, "data": "nightfall " }, + { "top": 426, "left": 205, "width": 12, "height": 23, "font": 0, "data": "to" }, + { "top": 442, "left": 60, "width": 88, "height": 23, "font": 0, "data": "Klausenburgh. " }, + { "top": 442, "left": 151, "width": 28, "height": 23, "font": 0, "data": "Here " }, + { "top": 442, "left": 183, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 458, "left": 60, "width": 49, "height": 23, "font": 0, "data": "stopped " }, + { "top": 458, "left": 113, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 458, "left": 132, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 458, "left": 155, "width": 31, "height": 23, "font": 0, "data": "night " }, + { "top": 458, "left": 190, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 458, "left": 204, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 475, "left": 60, "width": 32, "height": 23, "font": 0, "data": "Hotel " }, + { "top": 475, "left": 95, "width": 43, "height": 23, "font": 0, "data": "Royale. " }, + { "top": 475, "left": 142, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 475, "left": 149, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 475, "left": 175, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 475, "left": 195, "width": 41, "height": 23, "font": 0, "data": "dinner," }, + { "top": 491, "left": 60, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 491, "left": 75, "width": 36, "height": 23, "font": 0, "data": "rather " }, + { "top": 491, "left": 114, "width": 44, "height": 23, "font": 0, "data": "supper, " }, + { "top": 491, "left": 162, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 491, "left": 172, "width": 47, "height": 23, "font": 0, "data": "chicken" }, + { "top": 507, "left": 60, "width": 31, "height": 23, "font": 0, "data": "done " }, + { "top": 507, "left": 95, "width": 16, "height": 23, "font": 0, "data": "up " }, + { "top": 507, "left": 114, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 507, "left": 150, "width": 25, "height": 23, "font": 0, "data": "way " }, + { "top": 507, "left": 177, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 507, "left": 206, "width": 19, "height": 23, "font": 0, "data": "red" }, + { "top": 523, "left": 60, "width": 46, "height": 23, "font": 0, "data": "pepper, " }, + { "top": 523, "left": 110, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 523, "left": 150, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 523, "left": 177, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 523, "left": 205, "width": 31, "height": 23, "font": 0, "data": "good" }, + { "top": 539, "left": 60, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 539, "left": 83, "width": 39, "height": 23, "font": 0, "data": "thirsty. " }, + { "top": 539, "left": 127, "width": 37, "height": 23, "font": 0, "data": "(Mem. " }, + { "top": 539, "left": 167, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 539, "left": 190, "width": 37, "height": 23, "font": 0, "data": "recipe" }, + { "top": 556, "left": 60, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 556, "left": 79, "width": 36, "height": 23, "font": 0, "data": "Mina.) " }, + { "top": 556, "left": 119, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 556, "left": 126, "width": 35, "height": 23, "font": 0, "data": "asked " }, + { "top": 556, "left": 165, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 556, "left": 187, "width": 39, "height": 23, "font": 0, "data": "waiter," }, + { "top": 572, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 572, "left": 86, "width": 15, "height": 23, "font": 0, "data": "he " }, + { "top": 572, "left": 105, "width": 24, "height": 23, "font": 0, "data": "said " }, + { "top": 572, "left": 133, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 572, "left": 143, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 572, "left": 170, "width": 36, "height": 23, "font": 0, "data": "called" }, + { + "top": 588, + "left": 60, + "width": 49, + "height": 23, + "font": 0, + "data": "\u0026quot;paprika " + }, + { + "top": 588, + "left": 113, + "width": 42, + "height": 23, + "font": 0, + "data": "hendl,\u0026quot; " + }, + { "top": 588, "left": 159, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 588, "left": 185, "width": 26, "height": 23, "font": 0, "data": "that, " }, + { "top": 588, "left": 214, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 588, "left": 231, "width": 7, "height": 23, "font": 0, "data": "it" }, + { "top": 604, "left": 60, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 604, "left": 87, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 604, "left": 97, "width": 49, "height": 23, "font": 0, "data": "national " }, + { "top": 604, "left": 149, "width": 28, "height": 23, "font": 0, "data": "dish, " }, + { "top": 604, "left": 181, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 604, "left": 187, "width": 41, "height": 23, "font": 0, "data": "should" }, + { "top": 620, "left": 60, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 620, "left": 79, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 620, "left": 108, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 620, "left": 123, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 620, "left": 145, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 620, "left": 156, "width": 59, "height": 23, "font": 0, "data": "anywhere" }, + { "top": 637, "left": 60, "width": 34, "height": 23, "font": 0, "data": "along " }, + { "top": 637, "left": 98, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 637, "left": 120, "width": 76, "height": 23, "font": 0, "data": "Carpathians." }, + { "top": 653, "left": 60, "width": 11, "height": 23, "font": 0, "data": "In " }, + { "top": 653, "left": 74, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 653, "left": 97, "width": 66, "height": 23, "font": 0, "data": "population " }, + { "top": 653, "left": 166, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 669, "left": 60, "width": 75, "height": 23, "font": 0, "data": "Transylvania " }, + { "top": 669, "left": 139, "width": 31, "height": 23, "font": 0, "data": "there " }, + { "top": 669, "left": 173, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 669, "left": 195, "width": 24, "height": 23, "font": 0, "data": "four" }, + { "top": 685, "left": 60, "width": 44, "height": 23, "font": 0, "data": "distinct " }, + { "top": 685, "left": 107, "width": 76, "height": 23, "font": 0, "data": "nationalities: " }, + { "top": 685, "left": 187, "width": 43, "height": 23, "font": 0, "data": "Saxons" }, + { "top": 701, "left": 60, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 701, "left": 74, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 701, "left": 97, "width": 38, "height": 23, "font": 0, "data": "South, " }, + { "top": 701, "left": 139, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 701, "left": 165, "width": 36, "height": 23, "font": 0, "data": "mixed " }, + { "top": 701, "left": 205, "width": 25, "height": 23, "font": 0, "data": "with" }, + { "top": 718, "left": 60, "width": 30, "height": 23, "font": 0, "data": "them " }, + { "top": 718, "left": 94, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 718, "left": 117, "width": 57, "height": 23, "font": 0, "data": "Wallachs, " }, + { "top": 718, "left": 178, "width": 26, "height": 23, "font": 0, "data": "who " }, + { "top": 718, "left": 207, "width": 18, "height": 23, "font": 0, "data": "are" }, + { "top": 734, "left": 60, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 734, "left": 83, "width": 78, "height": 23, "font": 0, "data": "descendants " }, + { "top": 734, "left": 164, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 734, "left": 179, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 750, "left": 60, "width": 51, "height": 23, "font": 0, "data": "Dacians; " }, + { "top": 750, "left": 114, "width": 52, "height": 23, "font": 0, "data": "Magyars " }, + { "top": 750, "left": 169, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 750, "left": 183, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 766, "left": 60, "width": 32, "height": 23, "font": 0, "data": "West, " }, + { "top": 766, "left": 96, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 766, "left": 123, "width": 52, "height": 23, "font": 0, "data": "Szekelys " }, + { "top": 766, "left": 178, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 766, "left": 192, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 782, "left": 60, "width": 25, "height": 23, "font": 0, "data": "East " }, + { "top": 782, "left": 88, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 782, "left": 115, "width": 37, "height": 23, "font": 0, "data": "North. " }, + { "top": 782, "left": 155, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 782, "left": 161, "width": 18, "height": 23, "font": 0, "data": "am " }, + { "top": 782, "left": 183, "width": 35, "height": 23, "font": 0, "data": "going" }, + { "top": 799, "left": 60, "width": 42, "height": 23, "font": 0, "data": "among " }, + { "top": 799, "left": 106, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 799, "left": 128, "width": 33, "height": 23, "font": 0, "data": "latter, " }, + { "top": 799, "left": 165, "width": 26, "height": 23, "font": 0, "data": "who " }, + { "top": 799, "left": 195, "width": 32, "height": 23, "font": 0, "data": "claim" }, + { "top": 815, "left": 60, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 815, "left": 75, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 815, "left": 93, "width": 67, "height": 23, "font": 0, "data": "descended " }, + { "top": 815, "left": 165, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 815, "left": 195, "width": 30, "height": 23, "font": 0, "data": "Attila" }, + { "top": 831, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 831, "left": 86, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 831, "left": 109, "width": 34, "height": 23, "font": 0, "data": "Huns. " }, + { "top": 831, "left": 147, "width": 25, "height": 23, "font": 0, "data": "This " }, + { "top": 831, "left": 175, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 831, "left": 204, "width": 15, "height": 23, "font": 0, "data": "be" }, + { "top": 847, "left": 60, "width": 16, "height": 23, "font": 0, "data": "so, " }, + { "top": 847, "left": 80, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 847, "left": 99, "width": 33, "height": 23, "font": 0, "data": "when " }, + { "top": 847, "left": 136, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 847, "left": 159, "width": 52, "height": 23, "font": 0, "data": "Magyars" }, + { "top": 863, "left": 60, "width": 66, "height": 23, "font": 0, "data": "conquered " }, + { "top": 863, "left": 130, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 863, "left": 152, "width": 47, "height": 23, "font": 0, "data": "country " }, + { "top": 863, "left": 202, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 863, "left": 216, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 880, "left": 60, "width": 52, "height": 23, "font": 0, "data": "eleventh " }, + { "top": 880, "left": 116, "width": 46, "height": 23, "font": 0, "data": "century " }, + { "top": 880, "left": 165, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 880, "left": 194, "width": 35, "height": 23, "font": 0, "data": "found" }, + { "top": 896, "left": 60, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 896, "left": 83, "width": 32, "height": 23, "font": 0, "data": "Huns " }, + { "top": 896, "left": 118, "width": 40, "height": 23, "font": 0, "data": "settled " }, + { "top": 896, "left": 162, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 896, "left": 176, "width": 10, "height": 23, "font": 0, "data": "it." }, + { "top": 917, "left": 60, "width": 52, "height": 23, "font": 1, "data": "FIRST" }, + { "top": 934, "left": 60, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 934, "left": 66, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 934, "left": 93, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 934, "left": 112, "width": 55, "height": 23, "font": 0, "data": "breakfast " }, + { "top": 934, "left": 171, "width": 31, "height": 23, "font": 0, "data": "more" }, + { "top": 950, "left": 60, "width": 47, "height": 23, "font": 0, "data": "paprika, " }, + { "top": 950, "left": 111, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 950, "left": 138, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 950, "left": 148, "width": 22, "height": 23, "font": 0, "data": "sort " }, + { "top": 950, "left": 174, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 966, "left": 60, "width": 52, "height": 23, "font": 0, "data": "porridge " }, + { "top": 966, "left": 115, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 966, "left": 130, "width": 35, "height": 23, "font": 0, "data": "maize " }, + { "top": 966, "left": 168, "width": 27, "height": 23, "font": 0, "data": "flour " }, + { "top": 966, "left": 199, "width": 36, "height": 23, "font": 0, "data": "which" }, + { "top": 982, "left": 60, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 982, "left": 89, "width": 24, "height": 23, "font": 0, "data": "said " }, + { "top": 982, "left": 117, "width": 24, "height": 23, "font": 0, "data": "was " }, + { + "top": 982, + "left": 144, + "width": 70, + "height": 23, + "font": 0, + "data": "\u0026quot;mamaliga\u0026quot;," + }, + { "top": 998, "left": 60, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 998, "left": 86, "width": 58, "height": 23, "font": 0, "data": "egg-plant " }, + { "top": 998, "left": 148, "width": 41, "height": 23, "font": 0, "data": "stuffed " }, + { "top": 998, "left": 193, "width": 25, "height": 23, "font": 0, "data": "with" }, + { "top": 1015, "left": 60, "width": 63, "height": 23, "font": 0, "data": "forcemeat, " }, + { "top": 1015, "left": 127, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 1015, "left": 137, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 1015, "left": 166, "width": 54, "height": 23, "font": 0, "data": "excellent" }, + { "top": 1031, "left": 60, "width": 28, "height": 23, "font": 0, "data": "dish, " }, + { "top": 1031, "left": 92, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 1031, "left": 131, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 1031, "left": 161, "width": 21, "height": 23, "font": 0, "data": "call" }, + { + "top": 1047, + "left": 60, + "width": 68, + "height": 23, + "font": 0, + "data": "\u0026quot;impletata\u0026quot;. " + }, + { "top": 1047, "left": 132, "width": 40, "height": 23, "font": 0, "data": "(Mem., " }, + { "top": 1047, "left": 175, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 1047, "left": 198, "width": 37, "height": 23, "font": 0, "data": "recipe" }, + { "top": 1063, "left": 60, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 1063, "left": 79, "width": 21, "height": 23, "font": 0, "data": "this " }, + { "top": 1063, "left": 104, "width": 30, "height": 23, "font": 0, "data": "also.)" }, + { "top": 1079, "left": 60, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 1079, "left": 66, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 1079, "left": 93, "width": 56, "height": 23, "font": 0, "data": "evidently " }, + { "top": 1079, "left": 152, "width": 58, "height": 23, "font": 0, "data": "expected, " }, + { "top": 1079, "left": 214, "width": 16, "height": 23, "font": 0, "data": "for" }, + { "top": 1096, "left": 60, "width": 33, "height": 23, "font": 0, "data": "when " }, + { "top": 1096, "left": 97, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 1096, "left": 103, "width": 20, "height": 23, "font": 0, "data": "got " }, + { "top": 1096, "left": 127, "width": 27, "height": 23, "font": 0, "data": "near " }, + { "top": 1096, "left": 157, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1096, "left": 179, "width": 28, "height": 23, "font": 0, "data": "door " }, + { "top": 1096, "left": 211, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 1112, "left": 60, "width": 33, "height": 23, "font": 0, "data": "faced " }, + { "top": 1112, "left": 97, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 1112, "left": 107, "width": 90, "height": 23, "font": 0, "data": "cheery-looking" }, + { "top": 1128, "left": 60, "width": 41, "height": 23, "font": 0, "data": "elderly " }, + { "top": 1128, "left": 104, "width": 44, "height": 23, "font": 0, "data": "woman " }, + { "top": 1128, "left": 152, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 1128, "left": 166, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1128, "left": 189, "width": 32, "height": 23, "font": 0, "data": "usual" }, + { "top": 1144, "left": 60, "width": 48, "height": 23, "font": 0, "data": "peasant " }, + { "top": 1144, "left": 111, "width": 73, "height": 23, "font": 0, "data": "dress--white" }, + { "top": 1160, "left": 60, "width": 87, "height": 23, "font": 0, "data": "undergarment " }, + { "top": 1160, "left": 150, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 1160, "left": 179, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 1160, "left": 189, "width": 27, "height": 23, "font": 0, "data": "long" }, + { "top": 1177, "left": 60, "width": 43, "height": 23, "font": 0, "data": "double " }, + { "top": 1177, "left": 106, "width": 38, "height": 23, "font": 0, "data": "apron, " }, + { "top": 1177, "left": 148, "width": 30, "height": 23, "font": 0, "data": "front, " }, + { "top": 1177, "left": 182, "width": 22, "height": 23, "font": 0, "data": "and" }, + { "top": 53, "left": 257, "width": 31, "height": 23, "font": 0, "data": "back, " }, + { "top": 53, "left": 293, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 53, "left": 308, "width": 53, "height": 23, "font": 0, "data": "coloured " }, + { "top": 53, "left": 365, "width": 26, "height": 23, "font": 0, "data": "stuff " }, + { "top": 53, "left": 394, "width": 34, "height": 23, "font": 0, "data": "fitting" }, + { "top": 70, "left": 257, "width": 40, "height": 23, "font": 0, "data": "almost " }, + { "top": 70, "left": 301, "width": 19, "height": 23, "font": 0, "data": "too " }, + { "top": 70, "left": 323, "width": 27, "height": 23, "font": 0, "data": "tight " }, + { "top": 70, "left": 354, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 70, "left": 374, "width": 55, "height": 23, "font": 0, "data": "modesty." }, + { "top": 86, "left": 257, "width": 35, "height": 23, "font": 0, "data": "When " }, + { "top": 86, "left": 297, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 86, "left": 303, "width": 33, "height": 23, "font": 0, "data": "came " }, + { "top": 86, "left": 339, "width": 31, "height": 23, "font": 0, "data": "close " }, + { "top": 86, "left": 374, "width": 21, "height": 23, "font": 0, "data": "she" }, + { "top": 102, "left": 257, "width": 41, "height": 23, "font": 0, "data": "bowed " }, + { "top": 102, "left": 303, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 102, "left": 329, "width": 27, "height": 23, "font": 0, "data": "said, " }, + { "top": 102, "left": 360, "width": 27, "height": 23, "font": 0, "data": "\u0026quot;The " }, + { "top": 102, "left": 391, "width": 26, "height": 23, "font": 0, "data": "Herr" }, + { + "top": 118, + "left": 257, + "width": 83, + "height": 23, + "font": 0, + "data": "Englishman?\u0026quot;" + }, + { "top": 134, "left": 257, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 134, "left": 264, "width": 35, "height": 23, "font": 0, "data": "found " }, + { "top": 134, "left": 303, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 134, "left": 324, "width": 64, "height": 23, "font": 0, "data": "smattering " }, + { "top": 134, "left": 392, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 151, "left": 257, "width": 47, "height": 23, "font": 0, "data": "German " }, + { "top": 151, "left": 308, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 151, "left": 337, "width": 37, "height": 23, "font": 0, "data": "useful " }, + { "top": 151, "left": 377, "width": 29, "height": 23, "font": 0, "data": "here," }, + { "top": 167, "left": 257, "width": 45, "height": 23, "font": 0, "data": "indeed, " }, + { "top": 167, "left": 306, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 167, "left": 312, "width": 30, "height": 23, "font": 0, "data": "don\u0027t " }, + { "top": 167, "left": 346, "width": 33, "height": 23, "font": 0, "data": "know " }, + { "top": 167, "left": 383, "width": 26, "height": 23, "font": 0, "data": "how " }, + { "top": 167, "left": 412, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 183, "left": 257, "width": 41, "height": 23, "font": 0, "data": "should " }, + { "top": 183, "left": 302, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 183, "left": 321, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 183, "left": 350, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 183, "left": 365, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 183, "left": 387, "width": 15, "height": 23, "font": 0, "data": "on" }, + { "top": 199, "left": 257, "width": 46, "height": 23, "font": 0, "data": "without " }, + { "top": 199, "left": 306, "width": 10, "height": 23, "font": 0, "data": "it." }, + { "top": 215, "left": 257, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 215, "left": 264, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 215, "left": 291, "width": 20, "height": 23, "font": 0, "data": "not " }, + { "top": 215, "left": 314, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 215, "left": 343, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 215, "left": 358, "width": 27, "height": 23, "font": 0, "data": "light " }, + { "top": 215, "left": 388, "width": 15, "height": 23, "font": 0, "data": "on " }, + { "top": 215, "left": 407, "width": 22, "height": 23, "font": 0, "data": "any" }, + { "top": 232, "left": 257, "width": 27, "height": 23, "font": 0, "data": "map " }, + { "top": 232, "left": 287, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 232, "left": 303, "width": 30, "height": 23, "font": 0, "data": "work " }, + { "top": 232, "left": 335, "width": 37, "height": 23, "font": 0, "data": "giving " }, + { "top": 232, "left": 376, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 232, "left": 399, "width": 32, "height": 23, "font": 0, "data": "exact" }, + { "top": 248, "left": 257, "width": 43, "height": 23, "font": 0, "data": "locality " }, + { "top": 248, "left": 304, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 248, "left": 318, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 248, "left": 341, "width": 37, "height": 23, "font": 0, "data": "Castle " }, + { "top": 248, "left": 382, "width": 49, "height": 23, "font": 0, "data": "Dracula," }, + { "top": 264, "left": 257, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 264, "left": 274, "width": 31, "height": 23, "font": 0, "data": "there " }, + { "top": 264, "left": 308, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 264, "left": 330, "width": 16, "height": 23, "font": 0, "data": "no " }, + { "top": 264, "left": 349, "width": 33, "height": 23, "font": 0, "data": "maps " }, + { "top": 264, "left": 385, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 264, "left": 399, "width": 21, "height": 23, "font": 0, "data": "this" }, + { "top": 280, "left": 257, "width": 47, "height": 23, "font": 0, "data": "country " }, + { "top": 280, "left": 307, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 280, "left": 323, "width": 18, "height": 23, "font": 0, "data": "yet " }, + { "top": 280, "left": 345, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 280, "left": 359, "width": 53, "height": 23, "font": 0, "data": "compare" }, + { "top": 296, "left": 257, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 296, "left": 286, "width": 20, "height": 23, "font": 0, "data": "our " }, + { "top": 296, "left": 310, "width": 26, "height": 23, "font": 0, "data": "own " }, + { "top": 296, "left": 339, "width": 52, "height": 23, "font": 0, "data": "Ordance " }, + { "top": 296, "left": 394, "width": 41, "height": 23, "font": 0, "data": "Survey" }, + { "top": 313, "left": 257, "width": 36, "height": 23, "font": 0, "data": "Maps; " }, + { "top": 313, "left": 297, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 313, "left": 320, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 313, "left": 326, "width": 35, "height": 23, "font": 0, "data": "found " }, + { "top": 313, "left": 365, "width": 23, "height": 23, "font": 0, "data": "that " }, + { "top": 313, "left": 392, "width": 42, "height": 23, "font": 0, "data": "Bistritz," }, + { "top": 329, "left": 257, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 329, "left": 280, "width": 26, "height": 23, "font": 0, "data": "post " }, + { "top": 329, "left": 309, "width": 30, "height": 23, "font": 0, "data": "town " }, + { "top": 329, "left": 343, "width": 41, "height": 23, "font": 0, "data": "named " }, + { "top": 329, "left": 388, "width": 15, "height": 23, "font": 0, "data": "by" }, + { "top": 345, "left": 257, "width": 37, "height": 23, "font": 0, "data": "Count " }, + { "top": 345, "left": 298, "width": 49, "height": 23, "font": 0, "data": "Dracula, " }, + { "top": 345, "left": 351, "width": 9, "height": 23, "font": 0, "data": "is " }, + { "top": 345, "left": 363, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 345, "left": 373, "width": 29, "height": 23, "font": 0, "data": "fairly " }, + { "top": 345, "left": 405, "width": 28, "height": 23, "font": 0, "data": "well-" }, + { "top": 361, "left": 257, "width": 41, "height": 23, "font": 0, "data": "known " }, + { "top": 361, "left": 302, "width": 36, "height": 23, "font": 0, "data": "place. " }, + { "top": 361, "left": 341, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 361, "left": 348, "width": 28, "height": 23, "font": 0, "data": "shall " }, + { "top": 361, "left": 379, "width": 31, "height": 23, "font": 0, "data": "enter" }, + { "top": 377, "left": 257, "width": 27, "height": 23, "font": 0, "data": "here " }, + { "top": 377, "left": 287, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 377, "left": 323, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 377, "left": 338, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 377, "left": 359, "width": 35, "height": 23, "font": 0, "data": "notes, " }, + { "top": 377, "left": 399, "width": 13, "height": 23, "font": 0, "data": "as" }, + { "top": 394, "left": 257, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 394, "left": 287, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 394, "left": 315, "width": 41, "height": 23, "font": 0, "data": "refresh " }, + { "top": 394, "left": 360, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 394, "left": 381, "width": 50, "height": 23, "font": 0, "data": "memory" }, + { "top": 410, "left": 257, "width": 33, "height": 23, "font": 0, "data": "when " }, + { "top": 410, "left": 294, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 410, "left": 301, "width": 22, "height": 23, "font": 0, "data": "talk " }, + { "top": 410, "left": 325, "width": 26, "height": 23, "font": 0, "data": "over " }, + { "top": 410, "left": 355, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 410, "left": 376, "width": 39, "height": 23, "font": 0, "data": "travels" }, + { "top": 426, "left": 257, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 426, "left": 286, "width": 32, "height": 23, "font": 0, "data": "Mina." }, + { "top": 442, "left": 257, "width": 7, "height": 23, "font": 0, "data": "3 " }, + { "top": 442, "left": 268, "width": 28, "height": 23, "font": 0, "data": "May. " }, + { "top": 442, "left": 300, "width": 73, "height": 23, "font": 0, "data": "Bistritz.--Left " }, + { "top": 442, "left": 377, "width": 45, "height": 23, "font": 0, "data": "Munich" }, + { "top": 458, "left": 257, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 458, "left": 272, "width": 26, "height": 23, "font": 0, "data": "8:35 " }, + { "top": 458, "left": 301, "width": 28, "height": 23, "font": 0, "data": "P.M., " }, + { "top": 458, "left": 333, "width": 15, "height": 23, "font": 0, "data": "on " }, + { "top": 458, "left": 351, "width": 5, "height": 23, "font": 0, "data": "1 " }, + { "top": 458, "left": 359, "width": 10, "height": 23, "font": 0, "data": "st " }, + { "top": 458, "left": 373, "width": 28, "height": 23, "font": 0, "data": "May," }, + { "top": 475, "left": 257, "width": 45, "height": 23, "font": 0, "data": "arriving " }, + { "top": 475, "left": 306, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 475, "left": 320, "width": 41, "height": 23, "font": 0, "data": "Vienna " }, + { "top": 475, "left": 365, "width": 29, "height": 23, "font": 0, "data": "early " }, + { "top": 475, "left": 398, "width": 25, "height": 23, "font": 0, "data": "next" }, + { "top": 491, "left": 257, "width": 54, "height": 23, "font": 0, "data": "morning; " }, + { "top": 491, "left": 315, "width": 41, "height": 23, "font": 0, "data": "should " }, + { "top": 491, "left": 359, "width": 29, "height": 23, "font": 0, "data": "have " }, + { "top": 491, "left": 392, "width": 41, "height": 23, "font": 0, "data": "arrived" }, + { "top": 507, "left": 257, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 507, "left": 272, "width": 28, "height": 23, "font": 0, "data": "6:46, " }, + { "top": 507, "left": 304, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 507, "left": 327, "width": 26, "height": 23, "font": 0, "data": "train " }, + { "top": 507, "left": 357, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 507, "left": 384, "width": 14, "height": 23, "font": 0, "data": "an " }, + { "top": 507, "left": 402, "width": 28, "height": 23, "font": 0, "data": "hour" }, + { "top": 523, "left": 257, "width": 24, "height": 23, "font": 0, "data": "late. " }, + { "top": 523, "left": 285, "width": 69, "height": 23, "font": 0, "data": "Buda-Pesth " }, + { "top": 523, "left": 358, "width": 38, "height": 23, "font": 0, "data": "seems " }, + { "top": 523, "left": 400, "width": 6, "height": 23, "font": 0, "data": "a" }, + { "top": 539, "left": 257, "width": 62, "height": 23, "font": 0, "data": "wonderful " }, + { "top": 539, "left": 322, "width": 35, "height": 23, "font": 0, "data": "place, " }, + { "top": 539, "left": 362, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 539, "left": 392, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 556, "left": 257, "width": 48, "height": 23, "font": 0, "data": "glimpse " }, + { "top": 556, "left": 309, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 556, "left": 348, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 556, "left": 355, "width": 20, "height": 23, "font": 0, "data": "got " }, + { "top": 556, "left": 378, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 556, "left": 393, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 556, "left": 403, "width": 27, "height": 23, "font": 0, "data": "from" }, + { "top": 572, "left": 257, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 572, "left": 280, "width": 26, "height": 23, "font": 0, "data": "train " }, + { "top": 572, "left": 310, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 572, "left": 336, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 572, "left": 359, "width": 25, "height": 23, "font": 0, "data": "little " }, + { "top": 572, "left": 388, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 572, "left": 394, "width": 34, "height": 23, "font": 0, "data": "could" }, + { "top": 588, "left": 257, "width": 28, "height": 23, "font": 0, "data": "walk " }, + { "top": 588, "left": 288, "width": 48, "height": 23, "font": 0, "data": "through " }, + { "top": 588, "left": 340, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 588, "left": 363, "width": 42, "height": 23, "font": 0, "data": "streets. " }, + { "top": 588, "left": 408, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 604, "left": 257, "width": 37, "height": 23, "font": 0, "data": "feared " }, + { "top": 604, "left": 299, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 604, "left": 314, "width": 16, "height": 23, "font": 0, "data": "go " }, + { "top": 604, "left": 333, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 604, "left": 361, "width": 15, "height": 23, "font": 0, "data": "far " }, + { "top": 604, "left": 380, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 604, "left": 410, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 620, "left": 257, "width": 43, "height": 23, "font": 0, "data": "station, " }, + { "top": 620, "left": 304, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 620, "left": 320, "width": 17, "height": 23, "font": 0, "data": "we " }, + { "top": 620, "left": 341, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 620, "left": 368, "width": 41, "height": 23, "font": 0, "data": "arrived " }, + { "top": 620, "left": 413, "width": 22, "height": 23, "font": 0, "data": "late" }, + { "top": 637, "left": 257, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 637, "left": 284, "width": 37, "height": 23, "font": 0, "data": "would " }, + { "top": 637, "left": 325, "width": 26, "height": 23, "font": 0, "data": "start " }, + { "top": 637, "left": 354, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 637, "left": 370, "width": 27, "height": 23, "font": 0, "data": "near " }, + { "top": 637, "left": 400, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 653, "left": 257, "width": 42, "height": 23, "font": 0, "data": "correct " }, + { "top": 653, "left": 303, "width": 26, "height": 23, "font": 0, "data": "time " }, + { "top": 653, "left": 332, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 653, "left": 348, "width": 53, "height": 23, "font": 0, "data": "possible." }, + { "top": 674, "left": 257, "width": 77, "height": 23, "font": 1, "data": "SECOND" }, + { "top": 691, "left": 257, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 691, "left": 264, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 691, "left": 291, "width": 20, "height": 23, "font": 0, "data": "not " }, + { "top": 691, "left": 314, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 691, "left": 343, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 691, "left": 358, "width": 27, "height": 23, "font": 0, "data": "light " }, + { "top": 691, "left": 388, "width": 15, "height": 23, "font": 0, "data": "on " }, + { "top": 691, "left": 407, "width": 22, "height": 23, "font": 0, "data": "any" }, + { "top": 707, "left": 257, "width": 27, "height": 23, "font": 0, "data": "map " }, + { "top": 707, "left": 287, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 707, "left": 303, "width": 30, "height": 23, "font": 0, "data": "work " }, + { "top": 707, "left": 335, "width": 37, "height": 23, "font": 0, "data": "giving " }, + { "top": 707, "left": 376, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 707, "left": 399, "width": 32, "height": 23, "font": 0, "data": "exact" }, + { "top": 723, "left": 257, "width": 43, "height": 23, "font": 0, "data": "locality " }, + { "top": 723, "left": 304, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 723, "left": 318, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 723, "left": 341, "width": 37, "height": 23, "font": 0, "data": "Castle " }, + { "top": 723, "left": 382, "width": 49, "height": 23, "font": 0, "data": "Dracula," }, + { "top": 739, "left": 257, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 739, "left": 274, "width": 31, "height": 23, "font": 0, "data": "there " }, + { "top": 739, "left": 308, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 739, "left": 330, "width": 16, "height": 23, "font": 0, "data": "no " }, + { "top": 739, "left": 349, "width": 33, "height": 23, "font": 0, "data": "maps " }, + { "top": 739, "left": 385, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 739, "left": 399, "width": 21, "height": 23, "font": 0, "data": "this" }, + { "top": 755, "left": 257, "width": 47, "height": 23, "font": 0, "data": "country " }, + { "top": 755, "left": 307, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 755, "left": 323, "width": 18, "height": 23, "font": 0, "data": "yet " }, + { "top": 755, "left": 345, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 755, "left": 359, "width": 53, "height": 23, "font": 0, "data": "compare" }, + { "top": 772, "left": 257, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 772, "left": 286, "width": 20, "height": 23, "font": 0, "data": "our " }, + { "top": 772, "left": 310, "width": 26, "height": 23, "font": 0, "data": "own " }, + { "top": 772, "left": 339, "width": 52, "height": 23, "font": 0, "data": "Ordance " }, + { "top": 772, "left": 394, "width": 41, "height": 23, "font": 0, "data": "Survey" }, + { "top": 788, "left": 257, "width": 36, "height": 23, "font": 0, "data": "Maps; " }, + { "top": 788, "left": 297, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 788, "left": 320, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 788, "left": 326, "width": 35, "height": 23, "font": 0, "data": "found " }, + { "top": 788, "left": 365, "width": 23, "height": 23, "font": 0, "data": "that " }, + { "top": 788, "left": 392, "width": 42, "height": 23, "font": 0, "data": "Bistritz," }, + { "top": 804, "left": 257, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 804, "left": 280, "width": 26, "height": 23, "font": 0, "data": "post " }, + { "top": 804, "left": 309, "width": 30, "height": 23, "font": 0, "data": "town " }, + { "top": 804, "left": 343, "width": 41, "height": 23, "font": 0, "data": "named " }, + { "top": 804, "left": 388, "width": 15, "height": 23, "font": 0, "data": "by" }, + { "top": 820, "left": 257, "width": 37, "height": 23, "font": 0, "data": "Count " }, + { "top": 820, "left": 298, "width": 49, "height": 23, "font": 0, "data": "Dracula, " }, + { "top": 820, "left": 351, "width": 9, "height": 23, "font": 0, "data": "is " }, + { "top": 820, "left": 363, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 820, "left": 373, "width": 29, "height": 23, "font": 0, "data": "fairly " }, + { "top": 820, "left": 405, "width": 28, "height": 23, "font": 0, "data": "well-" }, + { "top": 836, "left": 257, "width": 41, "height": 23, "font": 0, "data": "known " }, + { "top": 836, "left": 302, "width": 36, "height": 23, "font": 0, "data": "place. " }, + { "top": 836, "left": 341, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 836, "left": 348, "width": 28, "height": 23, "font": 0, "data": "shall " }, + { "top": 836, "left": 379, "width": 31, "height": 23, "font": 0, "data": "enter" }, + { "top": 853, "left": 257, "width": 27, "height": 23, "font": 0, "data": "here " }, + { "top": 853, "left": 287, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 853, "left": 323, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 853, "left": 338, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 853, "left": 359, "width": 35, "height": 23, "font": 0, "data": "notes, " }, + { "top": 853, "left": 399, "width": 13, "height": 23, "font": 0, "data": "as" }, + { "top": 869, "left": 257, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 869, "left": 287, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 869, "left": 315, "width": 41, "height": 23, "font": 0, "data": "refresh " }, + { "top": 869, "left": 360, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 869, "left": 381, "width": 50, "height": 23, "font": 0, "data": "memory" }, + { "top": 885, "left": 257, "width": 33, "height": 23, "font": 0, "data": "when " }, + { "top": 885, "left": 294, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 885, "left": 301, "width": 22, "height": 23, "font": 0, "data": "talk " }, + { "top": 885, "left": 325, "width": 26, "height": 23, "font": 0, "data": "over " }, + { "top": 885, "left": 355, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 885, "left": 376, "width": 39, "height": 23, "font": 0, "data": "travels" }, + { "top": 901, "left": 257, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 901, "left": 286, "width": 32, "height": 23, "font": 0, "data": "Mina." }, + { "top": 917, "left": 257, "width": 7, "height": 23, "font": 0, "data": "It " }, + { "top": 917, "left": 268, "width": 38, "height": 23, "font": 0, "data": "seems " }, + { "top": 917, "left": 310, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 917, "left": 325, "width": 18, "height": 23, "font": 0, "data": "me " }, + { "top": 917, "left": 346, "width": 23, "height": 23, "font": 0, "data": "that " }, + { "top": 917, "left": 373, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 917, "left": 396, "width": 40, "height": 23, "font": 0, "data": "further" }, + { "top": 934, "left": 257, "width": 25, "height": 23, "font": 0, "data": "east " }, + { "top": 934, "left": 285, "width": 22, "height": 23, "font": 0, "data": "you " }, + { "top": 934, "left": 311, "width": 16, "height": 23, "font": 0, "data": "go " }, + { "top": 934, "left": 330, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 934, "left": 353, "width": 31, "height": 23, "font": 0, "data": "more" }, + { "top": 950, "left": 257, "width": 70, "height": 23, "font": 0, "data": "unpunctual " }, + { "top": 950, "left": 330, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 950, "left": 352, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 950, "left": 375, "width": 35, "height": 23, "font": 0, "data": "trains." }, + { "top": 966, "left": 257, "width": 32, "height": 23, "font": 0, "data": "What " }, + { "top": 966, "left": 293, "width": 36, "height": 23, "font": 0, "data": "ought " }, + { "top": 966, "left": 332, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 966, "left": 361, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 966, "left": 376, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 966, "left": 395, "width": 10, "height": 23, "font": 0, "data": "in" }, + { "top": 982, "left": 257, "width": 42, "height": 23, "font": 0, "data": "China?" }, + { "top": 998, "left": 257, "width": 23, "height": 23, "font": 0, "data": "The " }, + { "top": 998, "left": 284, "width": 56, "height": 23, "font": 0, "data": "strangest " }, + { "top": 998, "left": 343, "width": 41, "height": 23, "font": 0, "data": "figures " }, + { "top": 998, "left": 387, "width": 17, "height": 23, "font": 0, "data": "we " }, + { "top": 998, "left": 408, "width": 24, "height": 23, "font": 0, "data": "saw" }, + { "top": 1015, "left": 257, "width": 29, "height": 23, "font": 0, "data": "were " }, + { "top": 1015, "left": 290, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1015, "left": 313, "width": 48, "height": 23, "font": 0, "data": "Slovaks, " }, + { "top": 1015, "left": 365, "width": 26, "height": 23, "font": 0, "data": "who " }, + { "top": 1015, "left": 394, "width": 29, "height": 23, "font": 0, "data": "were" }, + { "top": 1031, "left": 257, "width": 31, "height": 23, "font": 0, "data": "more " }, + { "top": 1031, "left": 291, "width": 57, "height": 23, "font": 0, "data": "barbarian " }, + { "top": 1031, "left": 352, "width": 26, "height": 23, "font": 0, "data": "than " }, + { "top": 1031, "left": 383, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1031, "left": 405, "width": 24, "height": 23, "font": 0, "data": "rest," }, + { "top": 1047, "left": 257, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 1047, "left": 286, "width": 27, "height": 23, "font": 0, "data": "their " }, + { "top": 1047, "left": 317, "width": 19, "height": 23, "font": 0, "data": "big " }, + { "top": 1047, "left": 339, "width": 52, "height": 23, "font": 0, "data": "cow-boy " }, + { "top": 1047, "left": 395, "width": 27, "height": 23, "font": 0, "data": "hats," }, + { "top": 1063, "left": 257, "width": 31, "height": 23, "font": 0, "data": "great " }, + { "top": 1063, "left": 292, "width": 39, "height": 23, "font": 0, "data": "baggy " }, + { "top": 1063, "left": 334, "width": 64, "height": 23, "font": 0, "data": "dirty-white" }, + { "top": 1079, "left": 257, "width": 51, "height": 23, "font": 0, "data": "trousers, " }, + { "top": 1079, "left": 312, "width": 33, "height": 23, "font": 0, "data": "white " }, + { "top": 1079, "left": 348, "width": 29, "height": 23, "font": 0, "data": "linen " }, + { "top": 1079, "left": 381, "width": 34, "height": 23, "font": 0, "data": "shirts," }, + { "top": 1096, "left": 257, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 1096, "left": 284, "width": 61, "height": 23, "font": 0, "data": "enormous " }, + { "top": 1096, "left": 348, "width": 36, "height": 23, "font": 0, "data": "heavy " }, + { "top": 1096, "left": 387, "width": 42, "height": 23, "font": 0, "data": "leather" }, + { "top": 1112, "left": 257, "width": 31, "height": 23, "font": 0, "data": "belts, " }, + { "top": 1112, "left": 293, "width": 37, "height": 23, "font": 0, "data": "nearly " }, + { "top": 1112, "left": 333, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 1112, "left": 343, "width": 23, "height": 23, "font": 0, "data": "foot " }, + { "top": 1112, "left": 370, "width": 31, "height": 23, "font": 0, "data": "wide, " }, + { "top": 1112, "left": 405, "width": 14, "height": 23, "font": 0, "data": "all" }, + { "top": 1128, "left": 257, "width": 50, "height": 23, "font": 0, "data": "studded " }, + { "top": 1128, "left": 311, "width": 26, "height": 23, "font": 0, "data": "over " }, + { "top": 1128, "left": 341, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 1128, "left": 369, "width": 32, "height": 23, "font": 0, "data": "brass " }, + { "top": 1128, "left": 405, "width": 30, "height": 23, "font": 0, "data": "nails." }, + { "top": 1144, "left": 257, "width": 30, "height": 23, "font": 0, "data": "They " }, + { "top": 1144, "left": 290, "width": 30, "height": 23, "font": 0, "data": "wore " }, + { "top": 1144, "left": 324, "width": 27, "height": 23, "font": 0, "data": "high " }, + { "top": 1144, "left": 354, "width": 36, "height": 23, "font": 0, "data": "boots, " }, + { "top": 1144, "left": 394, "width": 25, "height": 23, "font": 0, "data": "with" }, + { "top": 1160, "left": 257, "width": 27, "height": 23, "font": 0, "data": "their " }, + { "top": 1160, "left": 288, "width": 48, "height": 23, "font": 0, "data": "trousers " }, + { "top": 1160, "left": 339, "width": 41, "height": 23, "font": 0, "data": "tucked " }, + { "top": 1160, "left": 384, "width": 23, "height": 23, "font": 0, "data": "into" }, + { "top": 1177, "left": 257, "width": 33, "height": 23, "font": 0, "data": "them, " }, + { "top": 1177, "left": 294, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 1177, "left": 321, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 1177, "left": 347, "width": 27, "height": 23, "font": 0, "data": "long " }, + { "top": 1177, "left": 378, "width": 33, "height": 23, "font": 0, "data": "black" }, + { "top": 53, "left": 455, "width": 23, "height": 23, "font": 0, "data": "hair " }, + { "top": 53, "left": 480, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 53, "left": 507, "width": 36, "height": 23, "font": 0, "data": "heavy " }, + { "top": 53, "left": 546, "width": 33, "height": 23, "font": 0, "data": "black" }, + { "top": 69, "left": 455, "width": 76, "height": 23, "font": 0, "data": "moustaches. " }, + { "top": 69, "left": 534, "width": 30, "height": 23, "font": 0, "data": "They " }, + { "top": 69, "left": 567, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 69, "left": 589, "width": 26, "height": 23, "font": 0, "data": "very" }, + { "top": 86, "left": 455, "width": 74, "height": 23, "font": 0, "data": "picturesque, " }, + { "top": 86, "left": 533, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 86, "left": 556, "width": 16, "height": 23, "font": 0, "data": "do " }, + { "top": 86, "left": 576, "width": 20, "height": 23, "font": 0, "data": "not " }, + { "top": 86, "left": 599, "width": 26, "height": 23, "font": 0, "data": "look" }, + { "top": 102, "left": 455, "width": 90, "height": 23, "font": 0, "data": "prepossessing. " }, + { "top": 102, "left": 548, "width": 17, "height": 23, "font": 0, "data": "On " }, + { "top": 102, "left": 569, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 102, "left": 592, "width": 33, "height": 23, "font": 0, "data": "stage" }, + { "top": 118, "left": 455, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 118, "left": 484, "width": 37, "height": 23, "font": 0, "data": "would " }, + { "top": 118, "left": 525, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 118, "left": 544, "width": 17, "height": 23, "font": 0, "data": "set " }, + { "top": 118, "left": 564, "width": 34, "height": 23, "font": 0, "data": "down " }, + { "top": 118, "left": 602, "width": 11, "height": 23, "font": 0, "data": "at" }, + { "top": 134, "left": 455, "width": 30, "height": 23, "font": 0, "data": "once " }, + { "top": 134, "left": 488, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 134, "left": 504, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 134, "left": 540, "width": 18, "height": 23, "font": 0, "data": "old " }, + { "top": 134, "left": 563, "width": 47, "height": 23, "font": 0, "data": "Oriental" }, + { "top": 150, "left": 455, "width": 31, "height": 23, "font": 0, "data": "band " }, + { "top": 150, "left": 489, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 150, "left": 504, "width": 56, "height": 23, "font": 0, "data": "brigands. " }, + { "top": 150, "left": 564, "width": 30, "height": 23, "font": 0, "data": "They " }, + { "top": 150, "left": 597, "width": 21, "height": 23, "font": 0, "data": "are," }, + { "top": 167, "left": 455, "width": 55, "height": 23, "font": 0, "data": "however, " }, + { "top": 167, "left": 513, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 167, "left": 520, "width": 18, "height": 23, "font": 0, "data": "am " }, + { "top": 167, "left": 541, "width": 25, "height": 23, "font": 0, "data": "told, " }, + { "top": 167, "left": 571, "width": 26, "height": 23, "font": 0, "data": "very" }, + { "top": 183, "left": 455, "width": 54, "height": 23, "font": 0, "data": "harmless " }, + { "top": 183, "left": 512, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 183, "left": 538, "width": 36, "height": 23, "font": 0, "data": "rather " }, + { "top": 183, "left": 577, "width": 48, "height": 23, "font": 0, "data": "wanting" }, + { "top": 199, "left": 455, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 199, "left": 469, "width": 42, "height": 23, "font": 0, "data": "natural " }, + { "top": 199, "left": 514, "width": 82, "height": 23, "font": 0, "data": "self-assertion." }, + { "top": 215, "left": 455, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 215, "left": 461, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 215, "left": 488, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 215, "left": 502, "width": 32, "height": 23, "font": 0, "data": "hurry " }, + { "top": 215, "left": 537, "width": 58, "height": 23, "font": 0, "data": "breakfast, " }, + { "top": 215, "left": 599, "width": 16, "height": 23, "font": 0, "data": "for" }, + { "top": 231, "left": 455, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 231, "left": 477, "width": 26, "height": 23, "font": 0, "data": "train " }, + { "top": 231, "left": 507, "width": 41, "height": 23, "font": 0, "data": "started " }, + { "top": 231, "left": 552, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 231, "left": 562, "width": 25, "height": 23, "font": 0, "data": "little " }, + { "top": 231, "left": 591, "width": 39, "height": 23, "font": 0, "data": "before" }, + { "top": 248, "left": 455, "width": 33, "height": 23, "font": 0, "data": "eight, " }, + { "top": 248, "left": 492, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 248, "left": 507, "width": 36, "height": 23, "font": 0, "data": "rather " }, + { "top": 248, "left": 546, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 248, "left": 556, "width": 36, "height": 23, "font": 0, "data": "ought " }, + { "top": 248, "left": 595, "width": 12, "height": 23, "font": 0, "data": "to" }, + { "top": 264, "left": 455, "width": 29, "height": 23, "font": 0, "data": "have " }, + { "top": 264, "left": 487, "width": 31, "height": 23, "font": 0, "data": "done " }, + { "top": 264, "left": 522, "width": 16, "height": 23, "font": 0, "data": "so, " }, + { "top": 264, "left": 542, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 264, "left": 561, "width": 27, "height": 23, "font": 0, "data": "after" }, + { "top": 280, "left": 455, "width": 45, "height": 23, "font": 0, "data": "rushing " }, + { "top": 280, "left": 504, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 280, "left": 519, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 280, "left": 541, "width": 40, "height": 23, "font": 0, "data": "station " }, + { "top": 280, "left": 585, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 280, "left": 599, "width": 26, "height": 23, "font": 0, "data": "7:30 " }, + { "top": 280, "left": 629, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 296, "left": 455, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 296, "left": 481, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 296, "left": 496, "width": 13, "height": 23, "font": 0, "data": "sit " }, + { "top": 296, "left": 513, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 296, "left": 527, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 296, "left": 549, "width": 49, "height": 23, "font": 0, "data": "carriage " }, + { "top": 296, "left": 601, "width": 16, "height": 23, "font": 0, "data": "for" }, + { "top": 312, "left": 455, "width": 31, "height": 23, "font": 0, "data": "more " }, + { "top": 312, "left": 489, "width": 26, "height": 23, "font": 0, "data": "than " }, + { "top": 312, "left": 519, "width": 14, "height": 23, "font": 0, "data": "an " }, + { "top": 312, "left": 537, "width": 28, "height": 23, "font": 0, "data": "hour " }, + { "top": 312, "left": 568, "width": 39, "height": 23, "font": 0, "data": "before " }, + { "top": 312, "left": 611, "width": 17, "height": 23, "font": 0, "data": "we" }, + { "top": 329, "left": 455, "width": 38, "height": 23, "font": 0, "data": "began " }, + { "top": 329, "left": 497, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 329, "left": 512, "width": 36, "height": 23, "font": 0, "data": "move." }, + { "top": 350, "left": 455, "width": 56, "height": 23, "font": 1, "data": "THIRD" }, + { "top": 366, "left": 455, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 366, "left": 461, "width": 26, "height": 23, "font": 0, "data": "read " }, + { "top": 366, "left": 491, "width": 23, "height": 23, "font": 0, "data": "that " }, + { "top": 366, "left": 518, "width": 33, "height": 23, "font": 0, "data": "every " }, + { "top": 366, "left": 554, "width": 41, "height": 23, "font": 0, "data": "known" }, + { "top": 383, "left": 455, "width": 70, "height": 23, "font": 0, "data": "superstition " }, + { "top": 383, "left": 529, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 383, "left": 543, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 383, "left": 566, "width": 34, "height": 23, "font": 0, "data": "world " }, + { "top": 383, "left": 603, "width": 9, "height": 23, "font": 0, "data": "is" }, + { "top": 399, "left": 455, "width": 54, "height": 23, "font": 0, "data": "gathered " }, + { "top": 399, "left": 513, "width": 23, "height": 23, "font": 0, "data": "into " }, + { "top": 399, "left": 539, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 399, "left": 561, "width": 63, "height": 23, "font": 0, "data": "horseshoe" }, + { "top": 415, "left": 455, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 415, "left": 469, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 415, "left": 492, "width": 75, "height": 23, "font": 0, "data": "Carpathians, " }, + { "top": 415, "left": 571, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 415, "left": 587, "width": 7, "height": 23, "font": 0, "data": "if " }, + { "top": 415, "left": 598, "width": 7, "height": 23, "font": 0, "data": "it" }, + { "top": 431, "left": 455, "width": 29, "height": 23, "font": 0, "data": "were " }, + { "top": 431, "left": 487, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 431, "left": 510, "width": 38, "height": 23, "font": 0, "data": "centre " }, + { "top": 431, "left": 551, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 431, "left": 566, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 431, "left": 602, "width": 22, "height": 23, "font": 0, "data": "sort" }, + { "top": 447, "left": 455, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 447, "left": 469, "width": 70, "height": 23, "font": 0, "data": "imaginative " }, + { "top": 447, "left": 543, "width": 59, "height": 23, "font": 0, "data": "whirlpool; " }, + { "top": 447, "left": 606, "width": 7, "height": 23, "font": 0, "data": "if " }, + { "top": 447, "left": 616, "width": 14, "height": 23, "font": 0, "data": "so" }, + { "top": 464, "left": 455, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 464, "left": 476, "width": 24, "height": 23, "font": 0, "data": "stay " }, + { "top": 464, "left": 504, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 464, "left": 532, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 464, "left": 551, "width": 26, "height": 23, "font": 0, "data": "very" }, + { "top": 480, "left": 455, "width": 67, "height": 23, "font": 0, "data": "interesting. " }, + { "top": 480, "left": 525, "width": 40, "height": 23, "font": 0, "data": "(Mem., " }, + { "top": 480, "left": 569, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 480, "left": 575, "width": 29, "height": 23, "font": 0, "data": "must " }, + { "top": 480, "left": 608, "width": 20, "height": 23, "font": 0, "data": "ask" }, + { "top": 496, "left": 455, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 496, "left": 477, "width": 37, "height": 23, "font": 0, "data": "Count " }, + { "top": 496, "left": 518, "width": 14, "height": 23, "font": 0, "data": "all " }, + { "top": 496, "left": 535, "width": 35, "height": 23, "font": 0, "data": "about " }, + { "top": 496, "left": 573, "width": 37, "height": 23, "font": 0, "data": "them.)" }, + { "top": 512, "left": 455, "width": 11, "height": 23, "font": 0, "data": "In " }, + { "top": 512, "left": 469, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 512, "left": 492, "width": 66, "height": 23, "font": 0, "data": "population " }, + { "top": 512, "left": 561, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 528, "left": 455, "width": 75, "height": 23, "font": 0, "data": "Transylvania " }, + { "top": 528, "left": 534, "width": 31, "height": 23, "font": 0, "data": "there " }, + { "top": 528, "left": 568, "width": 18, "height": 23, "font": 0, "data": "are " }, + { "top": 528, "left": 590, "width": 24, "height": 23, "font": 0, "data": "four" }, + { "top": 545, "left": 455, "width": 44, "height": 23, "font": 0, "data": "distinct " }, + { "top": 545, "left": 502, "width": 76, "height": 23, "font": 0, "data": "nationalities: " }, + { "top": 545, "left": 581, "width": 43, "height": 23, "font": 0, "data": "Saxons" }, + { "top": 561, "left": 455, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 561, "left": 469, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 561, "left": 491, "width": 38, "height": 23, "font": 0, "data": "South, " }, + { "top": 561, "left": 533, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 561, "left": 560, "width": 36, "height": 23, "font": 0, "data": "mixed " }, + { "top": 561, "left": 599, "width": 25, "height": 23, "font": 0, "data": "with" }, + { "top": 577, "left": 455, "width": 30, "height": 23, "font": 0, "data": "them " }, + { "top": 577, "left": 489, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 577, "left": 511, "width": 57, "height": 23, "font": 0, "data": "Wallachs, " }, + { "top": 577, "left": 573, "width": 26, "height": 23, "font": 0, "data": "who " }, + { "top": 577, "left": 602, "width": 18, "height": 23, "font": 0, "data": "are" }, + { "top": 593, "left": 455, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 593, "left": 477, "width": 78, "height": 23, "font": 0, "data": "descendants " }, + { "top": 593, "left": 559, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 593, "left": 573, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 609, "left": 455, "width": 51, "height": 23, "font": 0, "data": "Dacians; " }, + { "top": 609, "left": 509, "width": 52, "height": 23, "font": 0, "data": "Magyars " }, + { "top": 609, "left": 564, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 609, "left": 578, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 626, "left": 455, "width": 32, "height": 23, "font": 0, "data": "West, " }, + { "top": 626, "left": 491, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 626, "left": 517, "width": 52, "height": 23, "font": 0, "data": "Szekelys " }, + { "top": 626, "left": 572, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 626, "left": 587, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 642, "left": 455, "width": 25, "height": 23, "font": 0, "data": "East " }, + { "top": 642, "left": 483, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 642, "left": 510, "width": 37, "height": 23, "font": 0, "data": "North. " }, + { "top": 642, "left": 550, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 642, "left": 556, "width": 18, "height": 23, "font": 0, "data": "am " }, + { "top": 642, "left": 578, "width": 35, "height": 23, "font": 0, "data": "going" }, + { "top": 658, "left": 455, "width": 42, "height": 23, "font": 0, "data": "among " }, + { "top": 658, "left": 501, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 658, "left": 523, "width": 33, "height": 23, "font": 0, "data": "latter, " }, + { "top": 658, "left": 560, "width": 26, "height": 23, "font": 0, "data": "who " }, + { "top": 658, "left": 589, "width": 32, "height": 23, "font": 0, "data": "claim" }, + { "top": 674, "left": 455, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 674, "left": 470, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 674, "left": 488, "width": 67, "height": 23, "font": 0, "data": "descended " }, + { "top": 674, "left": 560, "width": 27, "height": 23, "font": 0, "data": "from " }, + { "top": 674, "left": 590, "width": 30, "height": 23, "font": 0, "data": "Attila" }, + { "top": 690, "left": 455, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 690, "left": 481, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 690, "left": 504, "width": 34, "height": 23, "font": 0, "data": "Huns. " }, + { "top": 690, "left": 542, "width": 25, "height": 23, "font": 0, "data": "This " }, + { "top": 690, "left": 570, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 690, "left": 598, "width": 15, "height": 23, "font": 0, "data": "be" }, + { "top": 707, "left": 455, "width": 16, "height": 23, "font": 0, "data": "so, " }, + { "top": 707, "left": 475, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 707, "left": 494, "width": 33, "height": 23, "font": 0, "data": "when " }, + { "top": 707, "left": 531, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 707, "left": 553, "width": 52, "height": 23, "font": 0, "data": "Magyars" }, + { "top": 723, "left": 455, "width": 66, "height": 23, "font": 0, "data": "conquered " }, + { "top": 723, "left": 524, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 723, "left": 547, "width": 47, "height": 23, "font": 0, "data": "country " }, + { "top": 723, "left": 597, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 723, "left": 611, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 739, "left": 455, "width": 52, "height": 23, "font": 0, "data": "eleventh " }, + { "top": 739, "left": 510, "width": 46, "height": 23, "font": 0, "data": "century " }, + { "top": 739, "left": 559, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 739, "left": 589, "width": 35, "height": 23, "font": 0, "data": "found" }, + { "top": 755, "left": 455, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 755, "left": 477, "width": 32, "height": 23, "font": 0, "data": "Huns " }, + { "top": 755, "left": 512, "width": 40, "height": 23, "font": 0, "data": "settled " }, + { "top": 755, "left": 556, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 755, "left": 570, "width": 10, "height": 23, "font": 0, "data": "it." }, + { "top": 771, "left": 455, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 771, "left": 461, "width": 19, "height": 23, "font": 0, "data": "did " }, + { "top": 771, "left": 484, "width": 20, "height": 23, "font": 0, "data": "not " }, + { "top": 771, "left": 507, "width": 32, "height": 23, "font": 0, "data": "sleep " }, + { "top": 771, "left": 543, "width": 27, "height": 23, "font": 0, "data": "well, " }, + { "top": 771, "left": 573, "width": 43, "height": 23, "font": 0, "data": "though" }, + { "top": 788, "left": 455, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 788, "left": 476, "width": 23, "height": 23, "font": 0, "data": "bed " }, + { "top": 788, "left": 503, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 788, "left": 530, "width": 73, "height": 23, "font": 0, "data": "comfortable" }, + { "top": 804, "left": 455, "width": 50, "height": 23, "font": 0, "data": "enough, " }, + { "top": 804, "left": 508, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 804, "left": 527, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 804, "left": 534, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 804, "left": 560, "width": 13, "height": 23, "font": 0, "data": "all " }, + { "top": 804, "left": 577, "width": 29, "height": 23, "font": 0, "data": "sorts " }, + { "top": 804, "left": 609, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 820, "left": 455, "width": 35, "height": 23, "font": 0, "data": "queer " }, + { "top": 820, "left": 493, "width": 47, "height": 23, "font": 0, "data": "dreams. " }, + { "top": 820, "left": 544, "width": 35, "height": 23, "font": 0, "data": "There " }, + { "top": 820, "left": 582, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 820, "left": 609, "width": 6, "height": 23, "font": 0, "data": "a" }, + { "top": 836, "left": 455, "width": 23, "height": 23, "font": 0, "data": "dog " }, + { "top": 836, "left": 482, "width": 48, "height": 23, "font": 0, "data": "howling " }, + { "top": 836, "left": 534, "width": 13, "height": 23, "font": 0, "data": "all " }, + { "top": 836, "left": 551, "width": 31, "height": 23, "font": 0, "data": "night " }, + { "top": 836, "left": 586, "width": 36, "height": 23, "font": 0, "data": "under" }, + { "top": 852, "left": 455, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 852, "left": 476, "width": 51, "height": 23, "font": 0, "data": "window, " }, + { "top": 852, "left": 531, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 852, "left": 570, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 852, "left": 599, "width": 29, "height": 23, "font": 0, "data": "have" }, + { "top": 869, "left": 455, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 869, "left": 481, "width": 64, "height": 23, "font": 0, "data": "something " }, + { "top": 869, "left": 549, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 869, "left": 564, "width": 16, "height": 23, "font": 0, "data": "do " }, + { "top": 869, "left": 583, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 869, "left": 612, "width": 10, "height": 23, "font": 0, "data": "it;" }, + { "top": 885, "left": 455, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 885, "left": 470, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 885, "left": 480, "width": 26, "height": 23, "font": 0, "data": "may " }, + { "top": 885, "left": 509, "width": 29, "height": 23, "font": 0, "data": "have " }, + { "top": 885, "left": 541, "width": 30, "height": 23, "font": 0, "data": "been " }, + { "top": 885, "left": 575, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 901, "left": 455, "width": 47, "height": 23, "font": 0, "data": "paprika, " }, + { "top": 901, "left": 506, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 901, "left": 525, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 901, "left": 532, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 901, "left": 558, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 901, "left": 573, "width": 31, "height": 23, "font": 0, "data": "drink " }, + { "top": 901, "left": 607, "width": 16, "height": 23, "font": 0, "data": "up" }, + { "top": 917, "left": 455, "width": 14, "height": 23, "font": 0, "data": "all " }, + { "top": 917, "left": 471, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 917, "left": 494, "width": 34, "height": 23, "font": 0, "data": "water " }, + { "top": 917, "left": 531, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 917, "left": 545, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 917, "left": 566, "width": 39, "height": 23, "font": 0, "data": "carafe, " }, + { "top": 917, "left": 609, "width": 22, "height": 23, "font": 0, "data": "and" }, + { "top": 933, "left": 455, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 933, "left": 482, "width": 20, "height": 23, "font": 0, "data": "still " }, + { "top": 933, "left": 505, "width": 39, "height": 23, "font": 0, "data": "thirsty. " }, + { "top": 933, "left": 548, "width": 51, "height": 23, "font": 0, "data": "Towards" }, + { "top": 950, "left": 455, "width": 50, "height": 23, "font": 0, "data": "morning " }, + { "top": 950, "left": 509, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 950, "left": 516, "width": 29, "height": 23, "font": 0, "data": "slept " }, + { "top": 950, "left": 548, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 950, "left": 574, "width": 24, "height": 23, "font": 0, "data": "was" }, + { "top": 966, "left": 455, "width": 55, "height": 23, "font": 0, "data": "wakened " }, + { "top": 966, "left": 513, "width": 15, "height": 23, "font": 0, "data": "by " }, + { "top": 966, "left": 532, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 966, "left": 554, "width": 68, "height": 23, "font": 0, "data": "continuous" }, + { "top": 982, "left": 455, "width": 56, "height": 23, "font": 0, "data": "knocking " }, + { "top": 982, "left": 514, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 982, "left": 529, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 982, "left": 550, "width": 30, "height": 23, "font": 0, "data": "door, " }, + { "top": 982, "left": 584, "width": 14, "height": 23, "font": 0, "data": "so " }, + { "top": 982, "left": 601, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 998, "left": 455, "width": 36, "height": 23, "font": 0, "data": "guess " }, + { "top": 998, "left": 494, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 998, "left": 500, "width": 29, "height": 23, "font": 0, "data": "must " }, + { "top": 998, "left": 533, "width": 29, "height": 23, "font": 0, "data": "have " }, + { "top": 998, "left": 565, "width": 30, "height": 23, "font": 0, "data": "been" }, + { "top": 1014, "left": 455, "width": 51, "height": 23, "font": 0, "data": "sleeping " }, + { "top": 1014, "left": 510, "width": 49, "height": 23, "font": 0, "data": "soundly " }, + { "top": 1014, "left": 561, "width": 30, "height": 23, "font": 0, "data": "then." }, + { "top": 1031, "left": 455, "width": 19, "height": 23, "font": 0, "data": "We " }, + { "top": 1031, "left": 478, "width": 18, "height": 23, "font": 0, "data": "left " }, + { "top": 1031, "left": 499, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 1031, "left": 514, "width": 35, "height": 23, "font": 0, "data": "pretty " }, + { "top": 1031, "left": 552, "width": 31, "height": 23, "font": 0, "data": "good " }, + { "top": 1031, "left": 587, "width": 28, "height": 23, "font": 0, "data": "time," }, + { "top": 1047, "left": 455, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 1047, "left": 481, "width": 33, "height": 23, "font": 0, "data": "came " }, + { "top": 1047, "left": 517, "width": 27, "height": 23, "font": 0, "data": "after " }, + { "top": 1047, "left": 547, "width": 49, "height": 23, "font": 0, "data": "nightfall " }, + { "top": 1047, "left": 600, "width": 12, "height": 23, "font": 0, "data": "to" }, + { "top": 1063, "left": 455, "width": 88, "height": 23, "font": 0, "data": "Klausenburgh. " }, + { "top": 1063, "left": 546, "width": 28, "height": 23, "font": 0, "data": "Here " }, + { "top": 1063, "left": 578, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 1079, "left": 455, "width": 49, "height": 23, "font": 0, "data": "stopped " }, + { "top": 1079, "left": 508, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 1079, "left": 527, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1079, "left": 550, "width": 31, "height": 23, "font": 0, "data": "night " }, + { "top": 1079, "left": 584, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 1079, "left": 599, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 1095, "left": 455, "width": 32, "height": 23, "font": 0, "data": "Hotel " }, + { "top": 1095, "left": 490, "width": 43, "height": 23, "font": 0, "data": "Royale. " }, + { "top": 1095, "left": 537, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 1095, "left": 544, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 1095, "left": 570, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 1095, "left": 589, "width": 41, "height": 23, "font": 0, "data": "dinner," }, + { "top": 1112, "left": 455, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 1112, "left": 470, "width": 36, "height": 23, "font": 0, "data": "rather " }, + { "top": 1112, "left": 509, "width": 44, "height": 23, "font": 0, "data": "supper, " }, + { "top": 1112, "left": 557, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 1112, "left": 567, "width": 47, "height": 23, "font": 0, "data": "chicken" }, + { "top": 1128, "left": 455, "width": 31, "height": 23, "font": 0, "data": "done " }, + { "top": 1128, "left": 489, "width": 16, "height": 23, "font": 0, "data": "up " }, + { "top": 1128, "left": 508, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 1128, "left": 544, "width": 25, "height": 23, "font": 0, "data": "way " }, + { "top": 1128, "left": 572, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 1128, "left": 601, "width": 19, "height": 23, "font": 0, "data": "red" }, + { "top": 1144, "left": 455, "width": 46, "height": 23, "font": 0, "data": "pepper, " }, + { "top": 1144, "left": 505, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 1144, "left": 544, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 1144, "left": 571, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 1144, "left": 600, "width": 31, "height": 23, "font": 0, "data": "good" }, + { "top": 1160, "left": 455, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 1160, "left": 478, "width": 39, "height": 23, "font": 0, "data": "thirsty. " }, + { "top": 1160, "left": 521, "width": 37, "height": 23, "font": 0, "data": "(Mem. " }, + { "top": 1160, "left": 562, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 1160, "left": 585, "width": 37, "height": 23, "font": 0, "data": "recipe" }, + { "top": 1176, "left": 455, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 1176, "left": 474, "width": 36, "height": 23, "font": 0, "data": "Mina.) " }, + { "top": 1176, "left": 514, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 1176, "left": 521, "width": 35, "height": 23, "font": 0, "data": "asked " }, + { "top": 1176, "left": 560, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 1176, "left": 582, "width": 39, "height": 23, "font": 0, "data": "waiter," }, + { "top": 53, "left": 652, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 53, "left": 678, "width": 15, "height": 23, "font": 0, "data": "he " }, + { "top": 53, "left": 697, "width": 24, "height": 23, "font": 0, "data": "said " }, + { "top": 53, "left": 725, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 53, "left": 735, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 53, "left": 762, "width": 36, "height": 23, "font": 0, "data": "called" }, + { + "top": 69, + "left": 652, + "width": 49, + "height": 23, + "font": 0, + "data": "\u0026quot;paprika " + }, + { + "top": 69, + "left": 705, + "width": 42, + "height": 23, + "font": 0, + "data": "hendl,\u0026quot; " + }, + { "top": 69, "left": 751, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 69, "left": 777, "width": 26, "height": 23, "font": 0, "data": "that, " }, + { "top": 69, "left": 806, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 69, "left": 823, "width": 7, "height": 23, "font": 0, "data": "it" }, + { "top": 86, "left": 652, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 86, "left": 679, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 86, "left": 689, "width": 49, "height": 23, "font": 0, "data": "national " }, + { "top": 86, "left": 741, "width": 28, "height": 23, "font": 0, "data": "dish, " }, + { "top": 86, "left": 773, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 86, "left": 779, "width": 41, "height": 23, "font": 0, "data": "should" }, + { "top": 102, "left": 652, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 102, "left": 671, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 102, "left": 700, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 102, "left": 715, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 102, "left": 737, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 102, "left": 748, "width": 59, "height": 23, "font": 0, "data": "anywhere" }, + { "top": 118, "left": 652, "width": 34, "height": 23, "font": 0, "data": "along " }, + { "top": 118, "left": 690, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 118, "left": 712, "width": 76, "height": 23, "font": 0, "data": "Carpathians." }, + { "top": 139, "left": 652, "width": 75, "height": 23, "font": 1, "data": "FOURTH" }, + { "top": 156, "left": 652, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 156, "left": 659, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 156, "left": 685, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 156, "left": 704, "width": 55, "height": 23, "font": 0, "data": "breakfast " }, + { "top": 156, "left": 763, "width": 31, "height": 23, "font": 0, "data": "more" }, + { "top": 172, "left": 652, "width": 47, "height": 23, "font": 0, "data": "paprika, " }, + { "top": 172, "left": 703, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 172, "left": 730, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 172, "left": 740, "width": 22, "height": 23, "font": 0, "data": "sort " }, + { "top": 172, "left": 766, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 188, "left": 652, "width": 52, "height": 23, "font": 0, "data": "porridge " }, + { "top": 188, "left": 707, "width": 12, "height": 23, "font": 0, "data": "of " }, + { "top": 188, "left": 722, "width": 35, "height": 23, "font": 0, "data": "maize " }, + { "top": 188, "left": 760, "width": 27, "height": 23, "font": 0, "data": "flour " }, + { "top": 188, "left": 791, "width": 36, "height": 23, "font": 0, "data": "which" }, + { "top": 204, "left": 652, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 204, "left": 681, "width": 24, "height": 23, "font": 0, "data": "said " }, + { "top": 204, "left": 709, "width": 24, "height": 23, "font": 0, "data": "was " }, + { + "top": 204, + "left": 736, + "width": 70, + "height": 23, + "font": 0, + "data": "\u0026quot;mamaliga\u0026quot;," + }, + { "top": 221, "left": 652, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 221, "left": 678, "width": 58, "height": 23, "font": 0, "data": "egg-plant " }, + { "top": 221, "left": 740, "width": 41, "height": 23, "font": 0, "data": "stuffed " }, + { "top": 221, "left": 785, "width": 25, "height": 23, "font": 0, "data": "with" }, + { "top": 237, "left": 652, "width": 63, "height": 23, "font": 0, "data": "forcemeat, " }, + { "top": 237, "left": 719, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 237, "left": 729, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 237, "left": 758, "width": 54, "height": 23, "font": 0, "data": "excellent" }, + { "top": 253, "left": 652, "width": 28, "height": 23, "font": 0, "data": "dish, " }, + { "top": 253, "left": 684, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 253, "left": 724, "width": 26, "height": 23, "font": 0, "data": "they " }, + { "top": 253, "left": 753, "width": 21, "height": 23, "font": 0, "data": "call" }, + { + "top": 269, + "left": 652, + "width": 68, + "height": 23, + "font": 0, + "data": "\u0026quot;impletata\u0026quot;. " + }, + { "top": 269, "left": 724, "width": 40, "height": 23, "font": 0, "data": "(Mem., " }, + { "top": 269, "left": 767, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 269, "left": 790, "width": 37, "height": 23, "font": 0, "data": "recipe" }, + { "top": 285, "left": 652, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 285, "left": 671, "width": 21, "height": 23, "font": 0, "data": "this " }, + { "top": 285, "left": 696, "width": 30, "height": 23, "font": 0, "data": "also.)" }, + { "top": 302, "left": 652, "width": 19, "height": 23, "font": 0, "data": "We " }, + { "top": 302, "left": 675, "width": 18, "height": 23, "font": 0, "data": "left " }, + { "top": 302, "left": 697, "width": 10, "height": 23, "font": 0, "data": "in " }, + { "top": 302, "left": 711, "width": 35, "height": 23, "font": 0, "data": "pretty " }, + { "top": 302, "left": 749, "width": 31, "height": 23, "font": 0, "data": "good " }, + { "top": 302, "left": 784, "width": 28, "height": 23, "font": 0, "data": "time," }, + { "top": 318, "left": 652, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 318, "left": 678, "width": 33, "height": 23, "font": 0, "data": "came " }, + { "top": 318, "left": 715, "width": 27, "height": 23, "font": 0, "data": "after " }, + { "top": 318, "left": 745, "width": 49, "height": 23, "font": 0, "data": "nightfall " }, + { "top": 318, "left": 797, "width": 12, "height": 23, "font": 0, "data": "to" }, + { "top": 334, "left": 652, "width": 88, "height": 23, "font": 0, "data": "Klausenburgh. " }, + { "top": 334, "left": 743, "width": 28, "height": 23, "font": 0, "data": "Here " }, + { "top": 334, "left": 775, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 350, "left": 652, "width": 49, "height": 23, "font": 0, "data": "stopped " }, + { "top": 350, "left": 705, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 350, "left": 725, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 350, "left": 747, "width": 31, "height": 23, "font": 0, "data": "night " }, + { "top": 350, "left": 782, "width": 11, "height": 23, "font": 0, "data": "at " }, + { "top": 350, "left": 796, "width": 19, "height": 23, "font": 0, "data": "the" }, + { "top": 366, "left": 652, "width": 32, "height": 23, "font": 0, "data": "Hotel " }, + { "top": 366, "left": 688, "width": 43, "height": 23, "font": 0, "data": "Royale. " }, + { "top": 366, "left": 735, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 366, "left": 741, "width": 22, "height": 23, "font": 0, "data": "had " }, + { "top": 366, "left": 767, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 366, "left": 787, "width": 41, "height": 23, "font": 0, "data": "dinner," }, + { "top": 383, "left": 652, "width": 12, "height": 23, "font": 0, "data": "or " }, + { "top": 383, "left": 667, "width": 36, "height": 23, "font": 0, "data": "rather " }, + { "top": 383, "left": 706, "width": 44, "height": 23, "font": 0, "data": "supper, " }, + { "top": 383, "left": 754, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 383, "left": 765, "width": 47, "height": 23, "font": 0, "data": "chicken" }, + { "top": 399, "left": 652, "width": 31, "height": 23, "font": 0, "data": "done " }, + { "top": 399, "left": 687, "width": 16, "height": 23, "font": 0, "data": "up " }, + { "top": 399, "left": 706, "width": 33, "height": 23, "font": 0, "data": "some " }, + { "top": 399, "left": 742, "width": 25, "height": 23, "font": 0, "data": "way " }, + { "top": 399, "left": 769, "width": 25, "height": 23, "font": 0, "data": "with " }, + { "top": 399, "left": 798, "width": 19, "height": 23, "font": 0, "data": "red" }, + { "top": 415, "left": 652, "width": 46, "height": 23, "font": 0, "data": "pepper, " }, + { "top": 415, "left": 702, "width": 36, "height": 23, "font": 0, "data": "which " }, + { "top": 415, "left": 742, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 415, "left": 769, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 415, "left": 797, "width": 31, "height": 23, "font": 0, "data": "good" }, + { "top": 431, "left": 652, "width": 20, "height": 23, "font": 0, "data": "but " }, + { "top": 431, "left": 675, "width": 39, "height": 23, "font": 0, "data": "thirsty. " }, + { "top": 431, "left": 719, "width": 37, "height": 23, "font": 0, "data": "(Mem. " }, + { "top": 431, "left": 759, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 431, "left": 782, "width": 37, "height": 23, "font": 0, "data": "recipe" }, + { "top": 447, "left": 652, "width": 16, "height": 23, "font": 0, "data": "for " }, + { "top": 447, "left": 671, "width": 36, "height": 23, "font": 0, "data": "Mina.) " }, + { "top": 447, "left": 711, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 447, "left": 718, "width": 35, "height": 23, "font": 0, "data": "asked " }, + { "top": 447, "left": 757, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 447, "left": 779, "width": 39, "height": 23, "font": 0, "data": "waiter," }, + { "top": 464, "left": 652, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 464, "left": 678, "width": 15, "height": 23, "font": 0, "data": "he " }, + { "top": 464, "left": 697, "width": 24, "height": 23, "font": 0, "data": "said " }, + { "top": 464, "left": 725, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 464, "left": 735, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 464, "left": 762, "width": 36, "height": 23, "font": 0, "data": "called" }, + { + "top": 480, + "left": 652, + "width": 49, + "height": 23, + "font": 0, + "data": "\u0026quot;paprika " + }, + { + "top": 480, + "left": 705, + "width": 42, + "height": 23, + "font": 0, + "data": "hendl,\u0026quot; " + }, + { "top": 480, "left": 751, "width": 22, "height": 23, "font": 0, "data": "and " }, + { "top": 480, "left": 777, "width": 26, "height": 23, "font": 0, "data": "that, " }, + { "top": 480, "left": 806, "width": 13, "height": 23, "font": 0, "data": "as " }, + { "top": 480, "left": 823, "width": 7, "height": 23, "font": 0, "data": "it" }, + { "top": 496, "left": 652, "width": 24, "height": 23, "font": 0, "data": "was " }, + { "top": 496, "left": 679, "width": 6, "height": 23, "font": 0, "data": "a " }, + { "top": 496, "left": 689, "width": 49, "height": 23, "font": 0, "data": "national " }, + { "top": 496, "left": 741, "width": 28, "height": 23, "font": 0, "data": "dish, " }, + { "top": 496, "left": 773, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 496, "left": 779, "width": 41, "height": 23, "font": 0, "data": "should" }, + { "top": 512, "left": 652, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 512, "left": 671, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 512, "left": 700, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 512, "left": 715, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 512, "left": 737, "width": 7, "height": 23, "font": 0, "data": "it " }, + { "top": 512, "left": 748, "width": 59, "height": 23, "font": 0, "data": "anywhere" }, + { "top": 528, "left": 652, "width": 34, "height": 23, "font": 0, "data": "along " }, + { "top": 528, "left": 690, "width": 19, "height": 23, "font": 0, "data": "the " }, + { "top": 528, "left": 712, "width": 76, "height": 23, "font": 0, "data": "Carpathians." }, + { "top": 545, "left": 652, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 545, "left": 659, "width": 35, "height": 23, "font": 0, "data": "found " }, + { "top": 545, "left": 698, "width": 18, "height": 23, "font": 0, "data": "my " }, + { "top": 545, "left": 719, "width": 64, "height": 23, "font": 0, "data": "smattering " }, + { "top": 545, "left": 786, "width": 12, "height": 23, "font": 0, "data": "of" }, + { "top": 561, "left": 652, "width": 47, "height": 23, "font": 0, "data": "German " }, + { "top": 561, "left": 703, "width": 26, "height": 23, "font": 0, "data": "very " }, + { "top": 561, "left": 732, "width": 37, "height": 23, "font": 0, "data": "useful " }, + { "top": 561, "left": 771, "width": 29, "height": 23, "font": 0, "data": "here," }, + { "top": 577, "left": 652, "width": 45, "height": 23, "font": 0, "data": "indeed, " }, + { "top": 577, "left": 701, "width": 3, "height": 23, "font": 0, "data": "I " }, + { "top": 577, "left": 707, "width": 30, "height": 23, "font": 0, "data": "don\u0027t " }, + { "top": 577, "left": 741, "width": 33, "height": 23, "font": 0, "data": "know " }, + { "top": 577, "left": 777, "width": 26, "height": 23, "font": 0, "data": "how " }, + { "top": 577, "left": 807, "width": 3, "height": 23, "font": 0, "data": "I" }, + { "top": 593, "left": 652, "width": 41, "height": 23, "font": 0, "data": "should " }, + { "top": 593, "left": 697, "width": 15, "height": 23, "font": 0, "data": "be " }, + { "top": 593, "left": 715, "width": 26, "height": 23, "font": 0, "data": "able " }, + { "top": 593, "left": 744, "width": 12, "height": 23, "font": 0, "data": "to " }, + { "top": 593, "left": 759, "width": 19, "height": 23, "font": 0, "data": "get " }, + { "top": 593, "left": 782, "width": 15, "height": 23, "font": 0, "data": "on" }, + { "top": 609, "left": 652, "width": 46, "height": 23, "font": 0, "data": "without " }, + { "top": 609, "left": 701, "width": 10, "height": 23, "font": 0, "data": "it." } + ] + } +] diff --git a/test/assets/text-order-mini.pdf b/test/assets/text-order-mini.pdf new file mode 100644 index 0000000000000000000000000000000000000000..70ceaaf8bcc0fa97ac4a967f8bb78dda164ba4df GIT binary patch literal 17804 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 Ks=E5SaRC5Iim<c* literal 0 HcmV?d00001 diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 00000000..c73e6a72 --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as clone from 'clone'; +import { readFileSync } from 'fs'; +import { PdfJsonExtractor } from '../server/src/extractors/pdf2json/PdfJsonExtractor'; +import { Module } from '../server/src/modules/Module'; +import { Config } from '../server/src/types/Config'; +import { Document } from '../server/src/types/DocumentRepresentation/Document'; + +export function getPdf( + func: (doc: Document) => Promise<Document>, + filename: string, +): Promise<[Document, Document]> { + const config: Config = JSON.parse(readFileSync(`${__dirname}/../server/defaultConfig.json`, 'utf8')); + + let docBefore: Document; + + return new PdfJsonExtractor(config) + .run(`${__dirname}/assets/${filename}`) + .then((doc: Document) => { + docBefore = clone(doc); + const docAfterPromise: Promise<Document> = func(doc); + return docAfterPromise; + }) + .then(docAfter => { + return [docBefore, docAfter] as [Document, Document]; // required because TS doesn't handle tuples correctly + }); +} + +export function runModules(originalDocument: Document, modules: Module[]): Promise<Document> { + return runNextModule(originalDocument, 0); + + function runNextModule(document: Document, i: number): Promise<Document> { + if (i < modules.length) { + return modules[i].run(document).then((newDoc: Document) => { + return runNextModule(newDoc, i + 1); + }); + } else { + return Promise.resolve(document); + } + } +} diff --git a/test/json-export-import.spec.ts b/test/json-export-import.spec.ts new file mode 100644 index 00000000..d4ac6e55 --- /dev/null +++ b/test/json-export-import.spec.ts @@ -0,0 +1,83 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { withData } from 'leche'; +import 'mocha'; +import { JsonExporter } from '../server/src/exporters/JsonExporter'; +import { HeadingDetectionModule } from '../server/src/modules/HeadingDetectionModule'; +import { HierarchyDetectionModule } from '../server/src/modules/HierarchyDetectionModule'; +import { LinesToParagraphModule } from '../server/src/modules/LinesToParagraphModule'; +import { ReadingOrderDetectionModule } from '../server/src/modules/ReadingOrderDetectionModule'; +import { WordsToLineModule } from '../server/src/modules/WordsToLineModule'; +import { Document, Element, JsonExport } from '../server/src/types/DocumentRepresentation'; +import { json2document } from '../server/src/utils/json2document'; +import { getPdf, runModules } from './helpers'; + +describe('JSON export and import', () => { + withData( + { + 'one line': 'line-merge.pdf', + 'big text': 'text-order-detection.pdf', + }, + pdfName => { + let pdfBefore: Document; + let pdfAfter: Document; + + function clean(pdf: Document): Promise<Document> { + Element.resetGlobalId(); + return runModules(pdf, [ + new ReadingOrderDetectionModule(), + new WordsToLineModule(), + new LinesToParagraphModule(), + new HeadingDetectionModule(), + new HierarchyDetectionModule(), + ]); + } + + before(done => { + function transform(pdf: Document): Promise<Document> { + return clean(pdf) + .then(doc => { + const jsonExporter = new JsonExporter(doc, 'word'); + const jsonDoc: JsonExport = jsonExporter.getJson(); + pdfAfter = json2document(jsonDoc); + return pdfAfter; + }); + } + + getPdf(transform, pdfName).then(([pdfB, pdfA]) => { + clean(pdfB) + .then(pdfBClean => { + pdfAfter = pdfA; + pdfBefore = pdfBClean; + done(); + }); + }); + }); + + it('should not change the document structure', () => { + const jsonBefore: JsonExport = new JsonExporter(pdfBefore, 'word').getJson(); + const jsonAfter: JsonExport = new JsonExporter(pdfAfter, 'word').getJson(); + + const jsonObjBefore: object = jsonBefore; + const jsonObjAfter: object = jsonAfter; + + expect(jsonObjBefore).to.eql(jsonObjAfter); + }); + }, + ); +}); diff --git a/test/line-merge.spec.ts b/test/line-merge.spec.ts new file mode 100644 index 00000000..e53b01ca --- /dev/null +++ b/test/line-merge.spec.ts @@ -0,0 +1,63 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { withData } from 'leche'; +import 'mocha'; +import { ReadingOrderDetectionModule } from '../server/src/modules/ReadingOrderDetectionModule'; +import { WordsToLineModule } from '../server/src/modules/WordsToLineModule'; +import { Document, Element } from '../server/src/types/DocumentRepresentation'; +import { getPdf, runModules } from './helpers'; + +describe('Line merge function', () => { + withData( + { + 'one line': ['line-merge.pdf', 'I’m a sentence with multiple words. '], + 'justified text': [ + 'line-merge-2.pdf', + 'Lorem ipsum, sagittis a, dolor. Nullam turpis lacus.', + ], + }, + (pdfName, text) => { + let pdfAfter: Document; + + before(done => { + function transform(pdf: Document) { + return runModules(pdf, [new ReadingOrderDetectionModule(), new WordsToLineModule()]); + } + + getPdf(transform, pdfName).then(([, pdfA]) => { + pdfAfter = pdfA; + done(); + }); + }); + + it('should merge side-by-side words into a single block', () => { + expect(pdfAfter.pages[0].elements) + .to.be.an('array') + .and.to.be.of.length(1); + }); + + it('should not alter the content', () => { + expect( + (pdfAfter.pages[0].elements[0].content as Element[]).map(t => t.content).join(''), + ).to.be.equal(text); + }); + + // TODO Page should not have any Words left except in Lines (same for Paragraph-merge) + }, + ); +}); diff --git a/test/number-correction.spec.ts b/test/number-correction.spec.ts new file mode 100644 index 00000000..022e918a --- /dev/null +++ b/test/number-correction.spec.ts @@ -0,0 +1,151 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import { withData } from 'leche'; +import 'mocha'; +import { NumberCorrectionModule } from '../server/src/modules/NumberCorrectionModule'; +import { + BoundingBox, + Document, + Font, + Line, + Page, + Paragraph, + Table, + TableCell, + TableRow, + Word, +} from '../server/src/types/DocumentRepresentation'; +import { getPdf, runModules } from './helpers'; + +describe('Number correction from pdf', () => { + const pdfName = 'number-correction-1.pdf'; + let pdfAfter: Document; + + before(done => { + function transform(doc: Document) { + return runModules(doc, [new NumberCorrectionModule()]); + } + + getPdf(transform, pdfName) + .then(([, pdfA]) => { + pdfAfter = pdfA; + done(); + }); + }); + + // this is dirty and we want to get number of tests from the test file itself + const testIds: number[] = Array.from(Array(11).keys()); + testIds.forEach(i => { + it(`should fix number misrecognition looking like 0.00`, () => { + const expected = pdfAfter.pages[0].elements[i].content; + expect(expected).to.be.equal('0.00'); + }); + }); +}); + +describe('Number correction from Abbyy-style table output', () => { + const dummyBB = new BoundingBox(0, 0, 0, 0); + const dummyFont = new Font('Arial', 12); + const testDocument: Document = new Document([ + new Page( + 0, + [ + new Table([ + new TableRow( + [ + new TableCell(dummyBB, [ + new Paragraph(dummyBB, [ + new Line(dummyBB, [new Word(dummyBB, '1234', dummyFont), new Word(dummyBB, '00', dummyFont)]), + ]), + ]), + ], + dummyBB, + ), + ]), + ], + dummyBB, + ), + ]); + + const numberCorrectionModule = new NumberCorrectionModule(); + + it(`should fix number misrecognition on testDocument`, () => { + const wordsBefore = testDocument.pages[0].getElementsOfType<Word>(Word); + expect(wordsBefore.length).to.be.equal(2); + numberCorrectionModule.run(testDocument).then(document => { + const wordsAfterCorrection = document.pages[0].getElementsOfType<Word>(Word); + expect(wordsAfterCorrection.length).to.be.equal(1); + expect(wordsAfterCorrection[0].content).to.be.equal('1234.00'); + }); + }); +}); + +function testableSuggest(numberCorrectionModule, input: string, regexp: RegExp, whitelist: Set<string>) { + const suggestions = numberCorrectionModule.suggestNumberCorrections(input, regexp, whitelist); + if (suggestions.length > 0) { + return suggestions[0][0]; + } + return input; +} + +describe('Single string number correction', () => { + const accountingFormat = RegExp('^([0-9]{1,3},([0-9]{3},)*[0-9]{3}|[0-9]+)(\\.[0-9]{2})?$'); + const emptyWhitelist = new Set<string>(); + const numberCorrectionModule = new NumberCorrectionModule(); + + withData(['ooo', 'OOO', 'o.O0', 'o.oo'], test => { + it(`should fix number misrecognition "${test}" looking like "0.00"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal('0.00'); + }); + }); + withData(['001', 'OOI'], test => { + it(`should fix number misrecognition "${test}" looking like "0.01"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal('0.01'); + }); + }); + withData(['9,999,99'], test => { + it(`should fix number misrecognition "${test}" looking like "9,999.99"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal('9,999.99'); + }); + }); + withData(['S', ' S'], test => { + it(`should fix number misrecognition "${test}" looking like "5"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal('5'); + }); + }); + withData(['155', ' ISS', '1SS'], test => { + it(`should fix number misrecognition "${test}" looking like "155"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal('155'); + }); + }); + withData(['ISS'], test => { + it(`should not change whitelisted word "${test}" looking like "155"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, new Set<string>(['ISS']))).to.be.equal(test); + }); + }); + withData(['wooooooops', 'ooooolala', 'pony'], test => { + it(`should not get any suggestions for legit words "${test}"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal(test); + }); + }); + withData(['4000', '1234.344', '1,234.23'], test => { + it(`should not change legit numbers "${test}"`, () => { + expect(testableSuggest(numberCorrectionModule, test, accountingFormat, emptyWhitelist)).to.be.equal(test); + }); + }); +}); diff --git a/test/paragraph-merge.spec.ts b/test/paragraph-merge.spec.ts new file mode 100644 index 00000000..4918757d --- /dev/null +++ b/test/paragraph-merge.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import 'mocha'; +import { LinesToParagraphModule } from '../server/src/modules/LinesToParagraphModule'; +import { ReadingOrderDetectionModule } from '../server/src/modules/ReadingOrderDetectionModule'; +import { WhitespaceRemovalModule } from '../server/src/modules/WhitespaceRemovalModule'; +import { WordsToLineModule } from '../server/src/modules/WordsToLineModule'; +import { Paragraph, Word } from '../server/src/types/DocumentRepresentation'; +import { Document } from '../server/src/types/DocumentRepresentation/Document'; +import { getPdf, runModules } from './helpers'; + +const pdfName = 'paragraph-merge.pdf'; + +describe('Paragraph merge function', () => { + let docBefore: Document; + let docAfter: Document; + + before(done => { + function cleaner(doc: Document) { + return runModules(doc, [ + new WhitespaceRemovalModule(), + new ReadingOrderDetectionModule(), + new WordsToLineModule(), + new WhitespaceRemovalModule(), + new LinesToParagraphModule({ addNewline: false }), + ]); + } + + getPdf(cleaner, pdfName).then(([docB, docA]) => { + docBefore = docB; + docAfter = docA; + done(); + }); + }); + + it('should merge side-by-side lines into paragraphs', () => { + expect(docAfter.pages[0].getElementsOfType<Paragraph>(Paragraph)) + .to.be.an('array') + .and.to.be.of.length(4); + }); + + it('should not alter the content', () => { + const contentBefore = docBefore.pages[0] + .getElementsOfType<Word>(Word) + .map(w => w.toString().trim()) + .join(' '); + const contentAfter = docAfter.pages[0].elements.join(' '); + expect(contentAfter).to.be.equal(contentBefore); + }); +}); diff --git a/test/redundancy-detection.spec.ts b/test/redundancy-detection.spec.ts new file mode 100644 index 00000000..ff2334c0 --- /dev/null +++ b/test/redundancy-detection.spec.ts @@ -0,0 +1,45 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import 'mocha'; +import { RedundancyDetectionModule } from '../server/src/modules/RedundancyDetectionModule'; +import { Word } from '../server/src/types/DocumentRepresentation'; +import { Document } from '../server/src/types/DocumentRepresentation/Document'; +import { getPdf, runModules } from './helpers'; + +const pdfName = 'redundancy-detection.pdf'; + +describe('Paragraph merge function', () => { + let doc: Document; + + before(done => { + getPdf((d) => runModules(d, [new RedundancyDetectionModule()]), pdfName).then(([, docAfter]) => { + doc = docAfter; + done(); + }); + }); + + it('should remove duplicates', () => { + const words: Word[] = doc.pages[0].getElementsOfType<Word>(Word); + expect(words) + .to.be.an('array') + .and.to.be.of.length(2); + + expect(words[0].toString().trim()).to.be.equal('Redundant'); + expect(words[1].toString().trim()).to.be.equal('Text'); + }); +}); diff --git a/test/text-order-detection.spec.ts b/test/text-order-detection.spec.ts new file mode 100644 index 00000000..c266f038 --- /dev/null +++ b/test/text-order-detection.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; +import 'mocha'; +import { ReadingOrderDetectionModule } from '../server/src/modules/ReadingOrderDetectionModule'; +import { Element } from '../server/src/types/DocumentRepresentation/Element'; +import { getPdf, runModules } from './helpers'; + +const pdfName = 'text-order-detection.pdf'; + +describe('Text order detection function', () => { + let first: Element; + let second: Element; + let third: Element; + let fourth: Element; + + before(done => { + getPdf(d => runModules(d, [new ReadingOrderDetectionModule()]), pdfName).then(([_, pdfAfter]) => { + pdfAfter.pages[0].elements.forEach(elt => { + switch (elt.content) { + case 'FIRST': + first = elt; + break; + case 'SECOND': + second = elt; + break; + case 'THIRD': + third = elt; + break; + case 'FOURTH': + fourth = elt; + break; + } + }); + done(); + }); + }); + + it('should have order properties', () => { + expect(first.properties.hasOwnProperty('order')); + expect(second.properties.hasOwnProperty('order')); + expect(third.properties.hasOwnProperty('order')); + expect(fourth.properties.hasOwnProperty('order')); + }); + + it('should be in the right order', () => { + expect(first.properties.order).to.be.lessThan(second.properties.order); + expect(second.properties.order).to.be.lessThan(third.properties.order); + expect(third.properties.order).to.be.lessThan(fourth.properties.order); + }); +}); diff --git a/test/utils.spec.ts b/test/utils.spec.ts new file mode 100644 index 00000000..e9eca2f1 --- /dev/null +++ b/test/utils.spec.ts @@ -0,0 +1,130 @@ +/** + * Copyright 2019 AXA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Line } from '../server/src/types/DocumentRepresentation/Line'; +import * as utils from '../server/src/utils'; +import { assert, expect } from 'chai'; +import 'mocha'; +import { withData } from 'leche'; +import { Word, BoundingBox, Text, Font } from '../server/src/types/DocumentRepresentation'; + +const dummyFont = new Font('Arial', 12); + +let t1 = new Word(new BoundingBox(10, 5, 30, 20), 'first', dummyFont); +t1.properties.order = 1; + +// We can't clone t1 as the _id_ is a read only property +// and will inherently be different for each object on instantiation +let t1Dup = new Word(new BoundingBox(10, 5, 30, 20), 'first', dummyFont); +t1Dup.properties.order = 1; + +let t2 = new Word(new BoundingBox(55, 60, 30, 40), 'second', dummyFont); +t2.properties.order = 2; +let t2Dup = new Word(new BoundingBox(55, 60, 30, 40), 'second', dummyFont); +t2Dup.properties.order = 2; + +let t3 = new Word(new BoundingBox(155, 160, 10, 5), 'third', dummyFont); +t3.properties.order = 3; +let t3Dup = new Word(new BoundingBox(155, 160, 10, 5), 'third', dummyFont); +t3Dup.properties.order = 3; + +describe('Utils sortTextsByOrder function', () => { + it('should sort texts', () => { + expect(utils.sortElementsByOrder(t1, t2)).to.be.lessThan(0); + expect(utils.sortElementsByOrder(t2, t1)).to.be.greaterThan(0); + }); +}); + +describe('Utils mergeText function', () => { + it('should merge texts', () => { + let parent = new Line(null); + let result: Text = utils.mergeElements(parent, t3, t1, t2); + let resultString = (result.content as Text[]).map(e => e.content).join(' '); + expect(resultString).to.be.equal('first second third'); + expect(result.top).to.be.equal(5); + expect(result.left).to.be.equal(10); + expect(result.height).to.be.equal(160); + expect(result.width).to.be.equal(155); + }); + + it('should not have side effects', () => { + function pseudoDeepCheck(elt1, elt2) { + expect(elt1.box).to.be.deep.equal(elt2.box); + expect(elt1.properties).to.be.deep.equal(elt2.properties); + expect(elt1.children).to.be.deep.equal(elt2.children); + expect(elt1.content).to.be.equal(elt2.content); + } + pseudoDeepCheck(t1, t1Dup); + pseudoDeepCheck(t2, t2Dup); + pseudoDeepCheck(t3, t3Dup); + }); + + it('should always return a Text', () => { + assert.instanceOf(utils.mergeElements(t1), Text); + assert.instanceOf(utils.mergeElements(t2, t3, t1), Text); + }); +}); + +describe('Utils getPageRegex function', () => { + withData( + [ + '3', + '03', + '(3)', + '[3]', + '[ 3 ]', + '-3-', + '- 3-', + '- 3 -', + 'iii', + '(iii)', + '- CIX -', + 'Page 3', + 'Page | 3', + '3 | Page', + 'Page 3 of 74', + '3 of 32', + 'pages 3-4', + '3 / 20', + 'Página 3 de 19', + '3 of\n21', + // '3 - General conditions PP 8601-01', + // '3 - General Terms and Conditions September 2016', + // 'Drive Motor Insurance Policy | 3', + // 'Tradesman’s Combined 3', + // 'PAGE NO.\n3\n77-53-46\nSIG\nSAN JUAN\nR', + // 'Marine Legal Protection 3', + ], + text => { + it('should return a regex that matches page numbers', () => { + assert(utils.getPageRegex().test(text)); + }); + it('should have a capturing group for the page number', () => { + let match = text.match(utils.getPageRegex()); + let pageNumber; + + for (let i = 1; i < match.length; i++) { + if (match[i]) { + pageNumber = match[i]; + } + } + + expect(pageNumber).to.exist; + expect(pageNumber).to.not.be.empty; + }); + }, + ); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..7b4ebccb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "files": ["server/bin/index.ts"], + "compilerOptions": { + "types": ["node"], + "lib": ["es7"], + "target": "es5", + "sourceMap": true, + "outDir": "dist", + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitAny": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noImplicitThis": true, + "strictNullChecks": false, + "downlevelIteration": true, + "resolveJsonModule": true + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 00000000..ccedae8a --- /dev/null +++ b/tslint.json @@ -0,0 +1,18 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended"], + "jsRules": {}, + "rules": { + "quotemark": false, + "interface-over-type-literal": [false], + "interface-name": [true, "never-prefix"], + "indent": false, + "forin": false, + "arrow-parens": false, + "object-literal-key-quotes": false, + "adjacent-overload-signatures": false, + "variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"], + "object-literal-sort-keys": [false] + }, + "rulesDirectory": [] +}