diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1165dd4e..00000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,46 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neuhart.julien@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] - -[homepage]: http://contributor-covenant.org -[version]: http://contributor-covenant.org/version/1/4/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 624d84f8..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,39 +0,0 @@ -# Contributing - -Hi! Thank you for considering contributing to Gotenberg. You'll -find below useful information about how to contribute to the Gotenberg project. - -## Contributing code - -### Install from sources - -1. Install and run the latest version of Docker -2. Verify your Go version (>= 1.14) -3. Fork this repository -4. Clone it outside of your `GOPATH` (we're using Go modules) - -### Working with git - -1. Create your feature branch (`git checkout -b my-new-feature`) -2. Commit your changes (`git commit -am 'Add some feature'`) -3. Push to the branch (`git push origin my-new-feature`) -4. Create a new pull request - -### Testing - -1. Run all linters (`make lint`) -2. Run all tests (`make tests`) - -## Reporting bugs and feature request - -Your issue or feature request may already be reported! -Please search on the [issue tracker](../../../issues) before creating one. - -If you do not find any relevant issue or feature request, feel free to -add a new one! - -## Additional resources - -* [Code of conduct](CODE_OF_CONDUCT.md) -* [Issue template](ISSUE_TEMPLATE.md) -* [Pull request template](PULL_REQUEST_TEMPLATE.md) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index fe595d44..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,35 +0,0 @@ -Your issue may already be reported! -Please search on the [issue tracker](../../../issues) before creating one. - -## Expected Behavior - - - -## Current Behavior - - - -## Possible Solution - - - -## Steps to Reproduce (for bugs) - - -1. -2. -3. -4. - -## Logs (LOG_LEVEL="DEBUG") - - -## Context - - - -## Your Environment - -* Version used: -* Operating System and version: -* Link to your project: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index e768c49b..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,45 +0,0 @@ -A similar PR may already be submitted! -Please search among the [pull requests](../../../pulls) before creating one. - -Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: - -For more information, see the [CONTRIBUTING](CONTRIBUTING.md) guide. - -**Summary** - - - -This PR fixes/implements the following **bugs/features** - -* [ ] Bug 1 -* [ ] Bug 2 -* [ ] Feature 1 -* [ ] Feature 2 -* [ ] Breaking changes - - - -Explain the **motivation** for making this change. What existing problem does the pull request solve? - - - -**Test plan (required)** - -Demonstrate the code is solid. Example: The exact commands you ran and their output. - - - -**Closing issues** - - -Fixes # - -**Checklist** - -- [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide? -- [ ] Have you lint your code locally prior to submission (`make lint`)? -- [ ] Have you written new tests for your core changes, as applicable? -- [ ] Have you successfully ran tests with your changes locally (`make tests`)? -- [ ] Have you updated the documentation (Markdown files under `build > docs > content` and then `make doc`)? -- [ ] I have squashed any insignificant commits -- [ ] This change has comments for package types, values, functions, and non-obvious lines of code diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 1fa95085..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: 2 -updates: -- package-ecosystem: gomod - directory: "/" - schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: github.com/microcosm-cc/bluemonday - versions: - - 1.0.4 - - 1.0.5 - - 1.0.6 - - 1.0.7 - - 1.0.8 - - dependency-name: golang.org/x/text - versions: - - 0.3.5 - - dependency-name: github.com/labstack/echo/v4 - versions: - - 4.1.17 - - 4.2.0 - - 4.2.1 - - dependency-name: github.com/sirupsen/logrus - versions: - - 1.7.0 - - 1.7.1 - - 1.8.0 - - dependency-name: github.com/mafredri/cdp - versions: - - 0.30.0 diff --git a/.github/workflows/tests_maybe_release.yml b/.github/workflows/tests_maybe_release.yml deleted file mode 100644 index b562239a..00000000 --- a/.github/workflows/tests_maybe_release.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: tests_maybe_release - -on: - push: - pull_request: - release: - types: [published] - -jobs: - tests_maybe_release: - runs-on: ubuntu-latest - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - uses: actions/checkout@v1 - - name: Lint code - run: make lint - - name: Run tests - run: | - make tests CODE_COVERAGE=1 - bash <(curl -s https://codecov.io/bash) - - name: Release - if: ${{ github.event_name == 'release' }} - run: make publish VERSION=${{ github.event.release.tag_name }} DOCKER_USER=${{ secrets.DOCKER_USER }} DOCKER_PASSWORD=${{ secrets.DOCKER_PASSWORD }} \ No newline at end of file diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 3c56ae0d..00000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.idea -coverage.txt \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index b756c5a5..00000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 TheCodingMachine - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 4478b495..00000000 --- a/Makefile +++ /dev/null @@ -1,66 +0,0 @@ -GOLANG_VERSION=1.14 -VERSION=snapshot -DOCKER_USER= -DOCKER_PASSWORD= -DOCKER_REGISTRY=thecodingmachine -GOTENBERG_USER_GID=1001 -GOTENBERG_USER_UID=1001 -GOLANGCI_LINT_VERSION=1.27.0 -CODE_COVERAGE=0 -TINI_VERSION=0.19.0 -MAXIMUM_WAIT_TIMEOUT=30.0 -MAXIMUM_WAIT_DELAY=10.0 -MAXIMUM_WEBHOOK_URL_TIMEOUT=30.0 -DEFAULT_WAIT_TIMEOUT=10.0 -DEFAULT_WEBHOOK_URL_TIMEOUT=10.0 -DEFAULT_LISTEN_PORT=3000 -DISABLE_GOOGLE_CHROME=0 -DISABLE_UNOCONV=0 -LOG_LEVEL=INFO -ROOT_PATH=/ -DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=1048576 -GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=0 - -# build the base Docker image. -base: - docker build --build-arg GOTENBERG_USER_GID=$(GOTENBERG_USER_GID) --build-arg GOTENBERG_USER_UID=$(GOTENBERG_USER_UID) -t $(DOCKER_REGISTRY)/gotenberg:base -f build/base/Dockerfile . - -# build the workspace Docker image. -workspace: - make base - docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:workspace -f build/workspace/Dockerfile . - -# gofmt and goimports all go files. -fmt: - go fmt ./... - go mod tidy - -# run all linters. -lint: - docker build --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:lint -f build/lint/Dockerfile . - docker run --rm $(DOCKER_REGISTRY)/gotenberg:lint - -# run all tests. -tests: - make workspace - ./scripts/tests.sh $(DOCKER_REGISTRY) $(CODE_COVERAGE) - -# generate documentation. -doc: - make workspace - docker build -t $(DOCKER_REGISTRY)/gotenberg:docs -f build/docs/Dockerfile . - docker run --rm -it -v "$(PWD):/gotenberg/docs" $(DOCKER_REGISTRY)/gotenberg:docs - -# build Gotenberg Docker image. -image: - make workspace - docker build --build-arg VERSION=$(VERSION) --build-arg TINI_VERSION=$(TINI_VERSION) -t $(DOCKER_REGISTRY)/gotenberg:$(VERSION) -f build/package/Dockerfile . - -# start the API using previously built Docker image. -gotenberg: - docker run -it --rm -e MAXIMUM_WAIT_TIMEOUT=$(MAXIMUM_WAIT_TIMEOUT) -e MAXIMUM_WAIT_DELAY=$(MAXIMUM_WAIT_DELAY) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_WEBHOOK_URL_TIMEOUT=$(DEFAULT_WEBHOOK_URL_TIMEOUT) -e MAXIMUM_WEBHOOK_URL_TIMEOUT=$(MAXIMUM_WEBHOOK_URL_TIMEOUT) -e DEFAULT_LISTEN_PORT=$(DEFAULT_LISTEN_PORT) -e DISABLE_GOOGLE_CHROME=$(DISABLE_GOOGLE_CHROME) -e DISABLE_UNOCONV=$(DISABLE_UNOCONV) -e LOG_LEVEL=$(LOG_LEVEL) -e ROOT_PATH=$(ROOT_PATH) -e DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE=$(DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE) -e GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS=$(GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS) -p "$(DEFAULT_LISTEN_PORT):$(DEFAULT_LISTEN_PORT)" $(DOCKER_REGISTRY)/gotenberg:$(VERSION) - -# publish Gotenberg images according to version. -publish: - make workspace - ./scripts/publish.sh $(GOLANG_VERSION) $(TINI_VERSION) $(DOCKER_REGISTRY) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD) \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 89d1bf7b..00000000 --- a/README.md +++ /dev/null @@ -1,45 +0,0 @@ -

- Gotenberg logo -

-

Gotenberg

-

A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.

-

Documentation · Contributing

- ---- - -At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from various sources. Each time, we ended up using some well known libraries and kind of lost time by reimplementing a solution from a project to another project. Meh. - -## Features - -* HTML and Markdown conversions using Google Chrome headless -* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv) -* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beautiful PDFs! -* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries - -## Quick start - -Open a terminal and run the following command: - -```bash -$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6 -``` - -The API is now available on your host at `http://localhost:3000`. - -Head to the [documentation](https://gotenberg.dev/) -to learn how to interact with it! - -## Badges - -[![Docker pulls](https://img.shields.io/docker/pulls/thecodingmachine/gotenberg)](https://hub.docker.com/r/thecodingmachine/gotenberg) -[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:6.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:6) -[![GitHub Actions](https://github.com/thecodingmachine/gotenberg/workflows/tests_maybe_release/badge.svg)](https://github.com/thecodingmachine/gotenberg/workflows/tests_maybe_release) -[![GoDoc](https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg)](https://godoc.org/github.com/thecodingmachine/gotenberg) -[![Codecov](https://codecov.io/gh/thecodingmachine/gotenberg/branch/master/graph/badge.svg)](https://codecov.io/gh/thecodingmachine/gotenberg) -[![Go Report Card](https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg)](https://goreportcard.com/report/thecodingmachine/gotenberg) - ---- - -

- Gotenberg logo -

diff --git a/assets/logo.png b/assets/logo.png deleted file mode 100644 index 4dacc2ee..00000000 Binary files a/assets/logo.png and /dev/null differ diff --git a/assets/logo2.png b/assets/logo2.png deleted file mode 100644 index de91ad06..00000000 Binary files a/assets/logo2.png and /dev/null differ diff --git a/build/base/Dockerfile b/build/base/Dockerfile deleted file mode 100644 index 85d2be8b..00000000 --- a/build/base/Dockerfile +++ /dev/null @@ -1,138 +0,0 @@ -FROM debian:buster-slim - -# |-------------------------------------------------------------------------- -# | Common libraries -# |-------------------------------------------------------------------------- -# | -# | Libraries used in the build process of this image. -# | -RUN apt-get update &&\ - apt-get --no-install-recommends install -y ca-certificates curl gnupg procps &&\ - rm -rf /var/lib/apt/lists/* - -# |-------------------------------------------------------------------------- -# | Microsoft font installer -# |-------------------------------------------------------------------------- -RUN apt-get update &&\ - curl -o ./ttf-mscorefonts-installer_3.8_all.deb https://httpredir.debian.org/debian/pool/contrib/m/msttcorefonts/ttf-mscorefonts-installer_3.8_all.deb &&\ - apt --no-install-recommends install -y ./ttf-mscorefonts-installer_3.8_all.deb && rm ./ttf-mscorefonts-installer_3.8_all.deb &&\ - rm -rf /var/lib/apt/lists/* - -# |-------------------------------------------------------------------------- -# | Chrome -# |-------------------------------------------------------------------------- -# | -# | Installs Chrome. -# | -RUN curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\ - echo "deb https://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\ - apt-get update &&\ - apt-get install --no-install-recommends -y --allow-unauthenticated google-chrome-stable &&\ - rm -rf /var/lib/apt/lists/* - -# |-------------------------------------------------------------------------- -# | LibreOffice -# |-------------------------------------------------------------------------- -# | -# | Installs LibreOffice. -# | - -# https://github.com/nextcloud/docker/issues/380 -RUN mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\ - echo "deb https://httpredir.debian.org/debian/ buster-backports main contrib non-free" >> /etc/apt/sources.list &&\ - apt-get update &&\ - apt-get --no-install-recommends -t buster-backports -y install libreoffice &&\ - rm -rf /var/lib/apt/lists/* - -# |-------------------------------------------------------------------------- -# | Unoconv -# |-------------------------------------------------------------------------- -# | -# | Installs unoconv. -# | - -ENV UNO_URL=https://raw.githubusercontent.com/dagwieers/unoconv/master/unoconv - -RUN curl -Ls $UNO_URL -o /usr/bin/unoconv &&\ - chmod +x /usr/bin/unoconv &&\ - ln -s /usr/bin/python3 /usr/bin/python &&\ - unoconv --version - -# |-------------------------------------------------------------------------- -# | PDFtk -# |-------------------------------------------------------------------------- -# | -# | Installs PDFtk as an alternative to pdfcpu for merging PDFs. -# | https://github.com/thecodingmachine/gotenberg/issues/29 -# | - -ARG PDFTK_VERSION=924565150 - -RUN curl -o /usr/bin/pdftk "https://gitlab.com/pdftk-java/pdftk/-/jobs/${PDFTK_VERSION}/artifacts/raw/build/native-image/pdftk" \ - && chmod a+x /usr/bin/pdftk - -# |-------------------------------------------------------------------------- -# | Fonts -# |-------------------------------------------------------------------------- -# | -# | Installs a handful of fonts. -# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile. -# | - -# Credits: -# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile -# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html -RUN apt-get update &&\ - apt-get install --no-install-recommends -y \ - culmus \ - fonts-beng \ - fonts-hosny-amiri \ - fonts-lklug-sinhala \ - fonts-lohit-guru \ - fonts-lohit-knda \ - fonts-samyak-gujr \ - fonts-samyak-mlym \ - fonts-samyak-taml \ - fonts-sarai \ - fonts-sil-abyssinica \ - fonts-sil-padauk \ - fonts-telu \ - fonts-thai-tlwg \ - ttf-wqy-zenhei \ - fonts-arphic-uming \ - fonts-ipafont-mincho \ - fonts-ipafont-gothic \ - fonts-unfonts-core \ - # LibreOffice recommends. - fonts-crosextra-caladea \ - fonts-crosextra-carlito \ - fonts-dejavu \ - fonts-dejavu-extra \ - fonts-liberation \ - fonts-liberation2 \ - fonts-linuxlibertine \ - fonts-noto-core \ - fonts-noto-mono \ - fonts-noto-ui-core \ - fonts-sil-gentium \ - fonts-sil-gentium-basic &&\ - rm -rf /var/lib/apt/lists/* - -COPY build/base/fonts/* /usr/local/share/fonts/ -COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf - -# |-------------------------------------------------------------------------- -# | Default user -# |-------------------------------------------------------------------------- -# | -# | All processes in the Docker container will run as a dedicated -# | non-root user. -# | - -ARG GOTENBERG_USER_GID=1001 -ARG GOTENBERG_USER_UID=1001 - -RUN groupadd --gid ${GOTENBERG_USER_GID} gotenberg \ - && useradd --uid ${GOTENBERG_USER_UID} --gid gotenberg --shell /bin/bash --home /gotenberg --no-create-home gotenberg \ - && mkdir /gotenberg \ - && chown gotenberg: /gotenberg diff --git a/build/base/fonts.conf b/build/base/fonts.conf deleted file mode 100644 index 344ccbef..00000000 --- a/build/base/fonts.conf +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - rgb - - - - - true - - - - - hintslight - - - - - true - - - - - lcddefault - - - \ No newline at end of file diff --git a/build/base/fonts/NotoColorEmoji.ttf b/build/base/fonts/NotoColorEmoji.ttf deleted file mode 100644 index 69cf21a1..00000000 Binary files a/build/base/fonts/NotoColorEmoji.ttf and /dev/null differ diff --git a/build/docs/Dockerfile b/build/docs/Dockerfile deleted file mode 100644 index 6c6a2df8..00000000 --- a/build/docs/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM thecodingmachine/gotenberg:workspace - -# |-------------------------------------------------------------------------- -# | static -# |-------------------------------------------------------------------------- -# | -# | Installs the static site anti-framework – general-purpose library, -# | purpose-built commands for various domains. -# | - -RUN go get github.com/apex/static/cmd/static-docs - -# |-------------------------------------------------------------------------- -# | Final touch -# |-------------------------------------------------------------------------- -# | -# | Last instructions of this build. -# | - -WORKDIR /gotenberg/docs - -CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ] \ No newline at end of file diff --git a/build/docs/content/00-introduction.md b/build/docs/content/00-introduction.md deleted file mode 100644 index 21b6a0a4..00000000 --- a/build/docs/content/00-introduction.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Introduction ---- - -[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF. - -* HTML and Markdown conversions using Google Chrome headless -* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv) -* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs! -* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg-go-client) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries diff --git a/build/docs/content/01-install.md b/build/docs/content/01-install.md deleted file mode 100644 index b56606b8..00000000 --- a/build/docs/content/01-install.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: Install ---- - -Gotenberg is shipped within a Docker image. - -**[Gotenberg should ONLY be used in a trusted network by trusted applications. Do NOT expose Gotenberg to the external world.](https://blazej-adamczyk.medium.com/0-day-bug-breaks-multi-million-dollar-system-38c9e31b27e9)** - -You may start it with: - -```bash -$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6 -``` - -> The API will be available at [http://localhost:3000](http://localhost:3000). - -The image uses a dedicated non-root user called `gotenberg` with uid and gid `1001`. - -If you wish to change those uid and gid, you will have to: - -* clone the project -* re-build the image -* publish the image in your own Docker registry - -For instance: - -```bash -$ git clone https://github.com/thecodingmachine/gotenberg.git -$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=version -``` - -> `master` branch is always up-to-date with the latest version of the API. - -## Docker Compose - -You may also add it in your Docker Compose stack: - -```yaml -version: '3' - -services: - - # your other services - - gotenberg: - image: thecodingmachine/gotenberg:6 -``` - -> The API will be available under `gotenberg:3000` in your Docker Compose network. - -## Kubernetes - -It may also be deployed with Kubernetes. - -Make sure to provide enough memory and CPU requests (for instance `512Mi` and `0.2` CPU). - -> The more resources are granted, the quicker will be the conversions. - -In the deployment specification of the pod, also specify the uid of the user `gotenberg`: - -``` -securityContext: - privileged: false - runAsUser: 1001 -``` - -## Cloud Run (Google Cloud) - -If you're looking for cost savings, you might be interested by [Cloud Run](https://cloud.google.com/run). -However, according to some users, doing asynchronous conversion (with a webhook) might not working. - -In the following examples, we will assume your -Gotenberg API is available at [http://localhost:3000](http://localhost:3000). diff --git a/build/docs/content/02-clients.md b/build/docs/content/02-clients.md deleted file mode 100644 index 0f4ce7d2..00000000 --- a/build/docs/content/02-clients.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -title: Clients ---- - -We provide clients in various languages for easing the interactions with the API. - -## Go client - -```bash -$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7 -``` - -See also the example from the [README](https://github.com/thecodingmachine/gotenberg-go-client/blob/master/README.md). - -## PHP client - -Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`: - -```bash -$ composer require php-http/guzzle6-adapter -``` - -Then the [PHP client](https://github.com/thecodingmachine/gotenberg-php-client): - -```bash -$ composer require thecodingmachine/gotenberg-php-client -``` - -See also the example from the [README](https://github.com/thecodingmachine/gotenberg-php-client/blob/master/README.md). - -## Community clients - -* [JavaScript/TypeScript client](https://github.com/yumauri/gotenberg-js-client) by [yumauri](https://github.com/yumauri) -* [C# client](https://github.com/ChangemakerStudios/GotenbergSharpApiClient) by [ChangemakerStudios](https://github.com/ChangemakerStudios) diff --git a/build/docs/content/03-environment-variables.md b/build/docs/content/03-environment-variables.md deleted file mode 100644 index 415dd020..00000000 --- a/build/docs/content/03-environment-variables.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: Environment variables ---- - -You may customize the API behaviour thanks to environment variables. - -## Log level - -The API provides structured logging allowing you to have relevant information -about what's going on. - -> If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format. - -You may customize the severity of the log entries thanks to the environment variable `LOG_LEVEL`. - -It accepts one of the following severities: `"DEBUG"`, `"INFO"` (default) and `"ERROR"`. - -## Default listen port - -By default, the API will listen on port `3000`. - -You may customize this value with the environment variable `DEFAULT_LISTEN_PORT`. - -This environment variable accepts any string that can be turned into a port number. - -## Root path - -By default, the API root path is `/`. - -You may customize this value with the environment variable `ROOT_PATH`. - -This environment variable accepts a string starting and ending with `/`. - -For instance, `/gotenberg/` is a valid value while `gotenberg` is not. - -> This is useful if you wish to do service discovery via URL paths. - -## Disable Google Chrome - -In order to save some resources, the Gotenberg image accepts the environment variable `DISABLE_GOOGLE_CHROME` -for disabling Google Chrome. - -It takes the strings `"0"` or `"1"` as value where `1` means `true` - -> If Google Chrome is disabled, the following conversions will **not** be available anymore: -> [HTML](#html), [URL](#url) and [Markdown](#markdown) - -## Default Google Chrome rpcc buffer size - -When performing a [HTML](#html), [URL](#url) or [Markdown](#markdown) conversion, the API might return -a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`. - -If so, you may increase this buffer size with the environment variable `DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE`. - -It takes a string representation of an int as value (e.g. `"1048576"` for 1 MB). -The hard limit is 100 MB and is defined by Google Chrome itself. - -> The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field `googleChromeRpccBufferSize`. -> See the [rpcc buffer size section](#html.rpcc_buffer_size). - -## Google Chrome ignore certificate errors - -When performing a [URL](#url) conversion, Google Chrome will not accept certificate errors. - -You may allow insecure connections by setting the `GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS` environment variable to `"1"`. - -**You should be careful with this feature and only enable it in your development environment.** - -## Disable LibreOffice (unoconv) - -You may also disable LibreOffice (unoconv) with `DISABLE_UNOCONV`. - -> If LibreOffice (unoconv) is disabled, the following conversion will **not** be available anymore: -> [Office](#office) - -## Default wait timeout - -By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful. -If unsucessful, it returns a `504` HTTP code. - -You may customize this timeout thanks to the environment variable `DEFAULT_WAIT_TIMEOUT`. - -It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds). - -> The default timeout may also be overridden per request thanks to the form field `waitTimeout`. -> See the [timeout section](#timeout). - -## Maximum wait timeout - -By default, the value of the form field `waitTimeout` cannot be more than 30 seconds. - -You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_TIMEOUT`. - -It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds). - -## Default webhook URL timeout - -By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful. - -> See the [webhook section](#webhook). - -You may customize this timeout thanks to the environment variable `DEFAULT_WEBHOOK_URL_TIMEOUT`. - -It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds). - -> The default timeout may also be overridden per request thanks to the form field `webhookURLTimeout`. -> See the [webhook timeout section](#webhook.timeout). - -## Maximum webhook URL timeout - -By default, the value of the form field `webhookURLTimeout` cannot be more than 30 seconds. - -You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WEBHOOK_URL_TIMEOUT`. - -It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds). - -## Maximum wait delay - -By default, the value of the form field `waitDelay` cannot be more than 10 seconds. - -> See the [wait delay section](#html.wait_delay). - -You may increase or decrease this limit thanks to the environment variable `MAXIMUM_WAIT_DELAY`. - -It takes a string representation of a float as value (e.g `"2.5"` for 2.5 seconds). diff --git a/build/docs/content/04-html.md b/build/docs/content/04-html.md deleted file mode 100644 index 688b8ae4..00000000 --- a/build/docs/content/04-html.md +++ /dev/null @@ -1,451 +0,0 @@ ---- -title: HTML ---- - -Gotenberg provides the endpoint `/convert/html` for HTML conversions. - -It accepts `POST` requests with a `multipart/form-data` Content-Type. - -## Basic - -The only requirement is to send a file named `index.html`: it is the file -which will be converted to PDF. - -For instance: - -```html - - - - - My PDF - - -

Hello world!

- - -``` - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Header and footer - -You may also add a header and/or a footer in the resulting PDF. -Respectively, a file named `header.html` and `footer.html`. - -Each of them **has to be a complete HTML document**: - -```html - - - - - -

- of -

- - -``` - -The following classes allow you to inject printing values: - -* `date`: formatted print date -* `title`: document title -* `pageNumber`: current page number -* `totalPage`: total pages in the document - -There are some limitations: - -* JavaScript is not executed -* external resources are not loaded -* the CSS properties are independant of the ones used in the `index.html` file -* `footer.html` CSS properties override the ones from `header.html` -* only fonts installed in the Docker image are loaded (see the [fonts section](#fonts)) -* images only work using a `base64` encoded source (`setHeader($header); -$request->setFooter($footer); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Assets - -You may also send additional files. For instance: images, fonts, stylesheets and so on. - -The only requirement is to make sure that their paths -are on the same level as the `index.html` file. - -In other words, this will work: - -```html - - - - - My PDF - - -

Hello world!

- - - -``` - -But this won't: - -```html - - - - - My PDF - - -

Hello world!

- - - -``` - -You may also use *remote* paths for Google fonts, images and so on. - -> If you want to install fonts directly in the Gotenberg Docker image, -> see to the [fonts section](#fonts). - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form files=@style.css \ - --form files=@img.png \ - --form files=@font.woff \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file") -img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file") -font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.Assets(style, img, font) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$assets = [ - DocumentFactory::makeFromPath('style.css', '/path/to/file'), - DocumentFactory::makeFromPath('img.png', '/path/to/file'), - DocumentFactory::makeFromPath('font.woff', '/path/to/file'), -]; -$request = new HTMLRequest($index); -$request->setAssets($assets); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Paper size, margins, orientation, scaling - -You may also customize the resulting PDF format. - -By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation and 100% (`1.0`) page scale. - -> Paper size and margins have to be provided in `inches`. Same for margins. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form paperWidth=8.27 \ - --form paperHeight=11.69 \ - --form marginTop=0 \ - --form marginBottom=0 \ - --form marginLeft=0 \ - --form marginRight=0 \ - --form landscape=true \ - --form scale=0.75 \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.PaperSize(gotenberg.A4) -req.Margins(gotenberg.NoMargins) -req.Landscape(true) -req.Scale(0.75) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setPaperSize(Request::A4); -$request->setMargins(Request::NO_MARGINS); -$request->setLandscape(true); -$request->setScale(0.75); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Page ranges - -You may specify the page ranges to convert. - -The format is the same as the one from the print options -of Google Chrome, e.g. `1-5,8,11-13`. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form pageRanges='1-3,5' \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.PageRanges("1-3,5") -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setPageRanges('1-3,5'); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Wait delay - -In some cases, you may want to wait a certain amount of time to make sure the -page you're trying to generate is fully rendered. For instance, if your page relies -a lot on JavaScript for rendering. - -> The wait delay is a duration in **seconds** (e.g `2.5` for 2.5 seconds). - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form waitDelay=5.5 \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.WaitDelay(5.5) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setWaitDelay(5.5); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Rpcc buffer size - -The API might return a `400` HTTP code with the message `increase the Google Chrome rpcc buffer size`. - -If so, you may increase this buffer size with a form field named `googleChromeRpccBufferSize`. - -It takes an int as value (e.g. `1048576` for 1 MB). -The hard limit is 100 MB and is defined by Google Chrome itself. - -> You may also define this value globally: see the [environment variables](#environment_variables.default_google_chrome_rpcc_buffer_size) section. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form googleChromeRpccBufferSize=1048576 \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.GoogleChromeRpccBufferSize(1048576) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setGoogleChromeRpccBufferSize(1048576); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/05-url.md b/build/docs/content/05-url.md deleted file mode 100644 index be892405..00000000 --- a/build/docs/content/05-url.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: URL ---- - -Gotenberg provides the endpoint `/convert/url` for remote URL conversions. - -It accepts `POST` requests with a `multipart/form-data` Content-Type. - -## Basic - -This endpoint does not accept an `index.html` file nor assets files but a form field -named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions. - -> **Attention:** when converting a website to PDF, you should remove all margins. -> If not, some of the content of the page might be hidden. - -> **Attention:** if you try to convert a URL from a Docker Compose service named `app` (i.e. `removeURL` = `http://app/an/entrypoint`), -> the resulting PDF will be blank. Make sure to rename your service to avoid this issue. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/url \ - --header 'Content-Type: multipart/form-data' \ - --form remoteURL=https://google.com \ - --form marginTop=0 \ - --form marginBottom=0 \ - --form marginLeft=0 \ - --form marginRight=0 \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -req := gotenberg.NewURLRequest("https://google.com") -req.Margins(gotenberg.NoMargins) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\URLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$request = new URLRequest('https://google.com'); -$request->setMargins(Request::NO_MARGINS); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Custom HTTP headers - -You may send your own HTTP headers to the `remoteURL`. - -For instance, by adding the HTTP header `Gotenberg-Remoteurl-Your-Header` to your request, -the API will send a request to the `remoteURL` with the HTTP header `Your-Header`. - -> **Attention:** the API uses a canonical format for the HTTP headers: -> it transforms the first -> letter and any letter following a hyphen to upper case; -> the rest are converted to lowercase. For example, the -> canonical key for `accept-encoding` is `Accept-Encoding`. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/url \ - --header 'Content-Type: multipart/form-data' \ - --header 'Gotenberg-Remoteurl-Your-Header: Foo' \ - --form remoteURL=https://google.com \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -req := gotenberg.NewURLRequest("https://google.com") -req.AddRemoteURLHTTPHeader("Your-Header", "Foo") -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\URLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$request = new URLRequest('https://google.com'); -$request->addRemoteURLHTTPHeader('Your-Header', 'Foo') -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/06-markdown.md b/build/docs/content/06-markdown.md deleted file mode 100644 index 5838ea7b..00000000 --- a/build/docs/content/06-markdown.md +++ /dev/null @@ -1,70 +0,0 @@ ---- -title: Markdown ---- - -Gotenberg provides the endpoint `/convert/markdown` for Markdown conversions. - -It accepts `POST` requests with a `multipart/form-data` Content-Type. - -## Basic - -Markdown conversions work the same as HTML conversions. - -Only difference is that you have access to the Go template function `toHTML` -in the file `index.html`. This function will convert a given markdown file to HTML. - -For instance: - -```html - - - - - My PDF - - - {{ toHTML .DirPath "file.md" }} - - -``` - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/markdown \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form files=@file.md \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file") -req := gotenberg.NewMarkdownRequest(index, markdown) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\MarkdownRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$markdowns = [ - DocumentFactory::makeFromPath('file.md', '/path/to/file'), -]; -$request = new MarkdownRequest($index, $markdowns); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/07-office.md b/build/docs/content/07-office.md deleted file mode 100644 index 1dbae793..00000000 --- a/build/docs/content/07-office.md +++ /dev/null @@ -1,167 +0,0 @@ ---- -title: Office ---- - -Gotenberg provides the endpoint `/convert/office` for Office document conversions. - -It accepts `POST` requests with a `multipart/form-data` Content-Type. - -## Basic - -You may send one or more Office documents. Following file extensions are accepted: - -* `.txt` -* `.rtf` -* `.fodt` -* `.doc` -* `.docx` -* `.odt` -* `.xls` -* `.xlsx` -* `.ods` -* `.ppt` -* `.pptx` -* `.odp` - -All files will be merged into a single resulting PDF. - -> **Attention:** Gotenberg merges the PDF files alphabetically. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/office \ - --header 'Content-Type: multipart/form-data' \ - --form files=@document.docx \ - --form files=@document2.docx \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") -doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file") -req := gotenberg.NewOfficeRequest(doc, doc2) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\OfficeRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$files = [ - DocumentFactory::makeFromPath('document.docx', '/path/to/file'), - DocumentFactory::makeFromPath('document2.docx', '/path/to/file'), -]; -$request = new OfficeRequest($files); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Orientation - -You may also customize the resulting PDF format. - -By default, it will be rendered with `portrait` orientation. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/office \ - --header 'Content-Type: multipart/form-data' \ - --form files=@document.docx \ - --form landscape=true \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") -req := gotenberg.NewOfficeRequest(doc) -req.Landscape(true) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\OfficeRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$files = [ - DocumentFactory::makeFromPath('document.docx', '/path/to/file'), -]; -$request = new OfficeRequest($files); -$request->setLandscape(true); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` - -## Page ranges - -You may specify the page ranges to convert. - -The format is the same as the one from the print options -of LibreOffice, e.g. `1-1` or `1-4`. - -> **Attention:** if more than one document, the page ranges will be -> applied for each document. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/office \ - --header 'Content-Type: multipart/form-data' \ - --form files=@document.docx \ - --form pageRanges='1-3' \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file") -req := gotenberg.NewOfficeRequest(doc) -req.PageRanges("1-3") -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\OfficeRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$files = [ - DocumentFactory::makeFromPath('document.docx', '/path/to/file'), -]; -$request = new OfficeRequest($files); -$request->setPageRanges('1-3'); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/08-merge.md b/build/docs/content/08-merge.md deleted file mode 100644 index 8e693743..00000000 --- a/build/docs/content/08-merge.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Merge ---- - -Gotenberg provides the endpoint `/merge` for merging PDFs. - -It accepts `POST` requests with a `multipart/form-data` Content-Type. - -## Basic - -Nothing fancy here: you may send one or more PDF files and the API -will merge them and return the resulting PDF file. - -> **Attention:** Gotenberg merges the PDF files alphabetically. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/merge \ - --header 'Content-Type: multipart/form-data' \ - --form files=@file.pdf \ - --form files=@file2.pdf \ - -o result.pdf -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file") -pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file") -req := gotenberg.NewMergeRequest(pdf, pdf2) -dest := "result.pdf" -c.Store(req, dest) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\MergeRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$files = [ - DocumentFactory::makeFromPath('file.pdf', '/path/to/file'), - DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'), -]; -$request = new MergeRequest($files); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/09-timeout.md b/build/docs/content/09-timeout.md deleted file mode 100644 index c7cbbd70..00000000 --- a/build/docs/content/09-timeout.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Timeout ---- - -All endpoints accept a form field named `waitTimeout`. - -The API will wait the given **seconds** before it considers the conversion to be unsucessful. -If unsucessful, it returns a `504` HTTP code. - -It takes a float as value (e.g `2.5` for 2.5 seconds). - -> You may also define this value globally: see the [environment variables](#environment_variables.default_wait_timeout) section. - -## Examples - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form waitTimeout=2.5 -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.WaitTimeout(2.5) -resp, _ := c.Post(req) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setWaitTimeout(2.5); -$dest = 'result.pdf'; -$client->store($request, $dest); -``` diff --git a/build/docs/content/10-webhook.md b/build/docs/content/10-webhook.md deleted file mode 100644 index 3d5e69e1..00000000 --- a/build/docs/content/10-webhook.md +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Webhook ---- - -All endpoints accept a form field named `webhookURL`. - -If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type -to given URL. - -By doing so, your requests to the API will be over before the conversions are actually done! - -## Examples - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form webhookURL='http://myapp.com/webhook/' -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.WebhookURL("http://myapp.com/webhook/") -resp, _ := c.Post(req) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setWebhookURL('http://myapp.com/webhook/'); -$resp = $client->post($request); -``` - -## Timeout - -If a `webhookURL` is provided, you may also send a form field named `webhookURLTimeout`. - -The API will wait the given **seconds** before it considers the sending of the resulting PDF to be unsucessful. - -It takes a float as value (e.g `2.5` for 2.5 seconds). - -> You may also define this value globally: see the [environment variables](#environment_variables.default_webhook_url_timeout) section. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form webhookURL='http://myapp.com/webhook/' \ - --form webhookURLTimeout=2.5 -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.WebhookURL("http://myapp.com/webhook/") -req.WebhookURLTimeout(2.5) -resp, _ := c.Post(req) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setWebhookURL('http://myapp.com/webhook/'); -$request->setWebhookURLTimeout(2.5); -$resp = $client->post($request); -``` - -## Custom HTTP headers - -You may send your own HTTP headers to the `webhookURL`. - -For instance, by adding the HTTP header `Gotenberg-Webhookurl-Your-Header` to your request, -the API will send a request to the `webhookURL` with the HTTP header `Your-Header`. - -> **Attention:** the API uses a canonical format for the HTTP headers: -> it transforms the first -> letter and any letter following a hyphen to upper case; -> the rest are converted to lowercase. For example, the -> canonical key for `accept-encoding` is `Accept-Encoding`. - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --header 'Gotenberg-Webhookurl-Your-Header: Foo' \ - --form files=@index.html \ - --form webhookURL='http://myapp.com/webhook/' -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.WebhookURL("http://myapp.com/webhook/") -req.AddWebhookURLHTTPHeader("Your-Header", "Foo") -resp, _ := c.Post(req) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setWebhookURL('http://myapp.com/webhook/'); -$request->addWebhookURLHTTPHeader('Your-Header', 'Foo'); -$resp = $client->post($request); -``` \ No newline at end of file diff --git a/build/docs/content/11-result-filename.md b/build/docs/content/11-result-filename.md deleted file mode 100644 index 94bc230f..00000000 --- a/build/docs/content/11-result-filename.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Result filename ---- - -All endpoints accept a form field named `resultFilename`. - -If provided, the API will return the resulting PDF file with the given filename. -Otherwise a random filename is used. - -> **Attention:** this feature does not work if the form field `webhookURL` is given. - -## Examples - -### cURL - -```bash -$ curl --request POST \ - --url http://localhost:3000/convert/html \ - --header 'Content-Type: multipart/form-data' \ - --form files=@index.html \ - --form resultFilename='foo.pdf' -``` - -### Go - -```golang -import "github.com/thecodingmachine/gotenberg-go-client/v7" - -c := &gotenberg.Client{Hostname: "http://localhost:3000"} -index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") -req := gotenberg.NewHTMLRequest(index) -req.ResultFilename("foo.pdf") -resp, _ := c.Post(req) -``` - -### PHP - -```php -use TheCodingMachine\Gotenberg\Client; -use TheCodingMachine\Gotenberg\DocumentFactory; -use TheCodingMachine\Gotenberg\HTMLRequest; -use TheCodingMachine\Gotenberg\Request; - -$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client()); -$index = DocumentFactory::makeFromPath('index.html', '/path/to/file'); -$request = new HTMLRequest($index); -$request->setResultFilename('foo.pdf'); -$resp = $client->post($request); -``` diff --git a/build/docs/content/12-scalability.md b/build/docs/content/12-scalability.md deleted file mode 100644 index 9c35545e..00000000 --- a/build/docs/content/12-scalability.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Scalability ---- - -The API uses under the hood intricate programs. - -Gotenberg tries to abstract as much complexity as possible but it can -only do it to a certain extent. - -For instance, [Office](#office) and [Merge](#merge) endpoints will start respectively as many LibreOffice (unoconv) and PDTk -instances as there are requests. The limitation here is the available memory and CPU usage. - -On another hand, for the [HTML](#html), [URL](#url) and [Markdown](#markdown) endpoints, the API does only 6 conversions in parallel. -Indeed, Google Chrome misbehaves if there are too many concurrent conversions. - -**The more concurrent requests, the more `504` HTTP codes the API will return.** - -> See our [load testing use case](https://github.com/thecodingmachine/gotenberg/tree/master/loadtesting) for more details about the API behaviour under heavy load. - -## Strategies - -### Increase timeout - -You may increase the conversion timeout. In other words, you accept that a conversion takes more time -if the API is under heavy load. - -> See [timeout section](#timeout). - -### Scaling - -The API being stateless, you may scale it as much as you want. - -For instance, using the following Docker Compose file: - -```yaml -version: '3' - -services: - - # your other services - - gotenberg: - image: thecodingmachine/gotenberg:6 -``` - -You may now launch your services using: - -```bash -$ docker-compose up --scale gotenberg=your_number_of_instances -``` - -When requesting the Gotenberg service with your client(s), Docker will automatically -redirect a request to a Gotenberg container according to the round-robin strategy. diff --git a/build/docs/content/13-ping.md b/build/docs/content/13-ping.md deleted file mode 100644 index 1535b607..00000000 --- a/build/docs/content/13-ping.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Ping ---- - -Gotenberg provides the endpoint `/ping` for checking the API availability with -a simple `GET` request. - -Currently this endpoint does nothing special. A better way to monitor -Gotenberg would be by checking the memory usage. - -Also, as the API uses under the hood intricate programs, you should -restart your Gotenberg instances from time to time to ensure a nominal behaviour. diff --git a/build/docs/content/14-fonts.md b/build/docs/content/14-fonts.md deleted file mode 100644 index 2a88ab11..00000000 --- a/build/docs/content/14-fonts.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Fonts ---- - -By default, a handful of fonts are installed. Asian characters are also supported out of the box. - -If you wish to use more fonts, you will have to create your own image: - -```Dockerfile -FROM thecodingmachine/gotenberg:6 - -USER root - -RUN apt-get -y install yourfonts - -USER gotenberg -``` diff --git a/build/docs/content/15-links.md b/build/docs/content/15-links.md deleted file mode 100644 index 68a855e8..00000000 --- a/build/docs/content/15-links.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Links ---- - -

- Gotenberg logo -

- -* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg) -* Follow [@gulnap](https://twitter.com/gulnap) on Twitter -* Thanks to [@mafredri](https://github.com/mafredri) for his help and his wonderful [cdp](https://github.com/mafredri/cdp) library -* OpenAPI documentation for the API can be found [on GitHub](https://github.com/thecodingmachine/gotenberg/blob/master/docs/openapi.yaml) diff --git a/build/lint/Dockerfile b/build/lint/Dockerfile deleted file mode 100644 index 0abf3718..00000000 --- a/build/lint/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -ARG GOLANGCI_LINT_VERSION - -FROM golangci/golangci-lint:v$GOLANGCI_LINT_VERSION - -# |-------------------------------------------------------------------------- -# | Final touch -# |-------------------------------------------------------------------------- -# | -# | Last instructions of this build. -# | - -# Define our working directory outside of $GOPATH (we're using go modules). -WORKDIR /gotenberg/lint - -# Copy our code source. -COPY . . - -# Install module dependencies. -RUN go mod download &&\ - go mod verify - -CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=funlen", "--disable=wsl", "--disable=gocognit", "--disable=gomnd", "--disable=goimports" ] \ No newline at end of file diff --git a/build/package/Dockerfile b/build/package/Dockerfile deleted file mode 100644 index 67a686d2..00000000 --- a/build/package/Dockerfile +++ /dev/null @@ -1,62 +0,0 @@ -# |-------------------------------------------------------------------------- -# | Binary -# |-------------------------------------------------------------------------- -# | -# | Builds Gotenberg binary. -# | - -FROM thecodingmachine/gotenberg:workspace AS workspace - -ARG VERSION - -ENV GOOS=linux \ - GOARCH=amd64 \ - CGO_ENABLED=0 - -# Define our workding outside of $GOPATH (we're using go modules). -WORKDIR /gotenberg/package - -# Install module dependencies. -COPY go.mod go.sum ./ - -RUN go mod download &&\ - go mod verify - -# Copy our source code. -COPY internal ./internal -COPY cmd ./cmd - -# Build our binary. -RUN go build -o gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go - -FROM thecodingmachine/gotenberg:base - -LABEL authors="Julien Neuhart " - -# |-------------------------------------------------------------------------- -# | Tini -# |-------------------------------------------------------------------------- -# | -# | An helper for reaping zombie processes. -# | - -ARG TINI_VERSION - -ADD https://github.com/krallin/tini/releases/download/v${TINI_VERSION}/tini-static /tini -RUN chmod +x /tini -ENTRYPOINT [ "/tini", "--" ] - -# |-------------------------------------------------------------------------- -# | Final touch -# |-------------------------------------------------------------------------- -# | -# | Last instructions of this build. -# | - -COPY --from=workspace /gotenberg/package/gotenberg /usr/local/bin/ - -USER gotenberg -WORKDIR /gotenberg - -EXPOSE 3000 -CMD [ "gotenberg" ] diff --git a/build/tests/Dockerfile b/build/tests/Dockerfile deleted file mode 100644 index cccf031a..00000000 --- a/build/tests/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM thecodingmachine/gotenberg:workspace - -# Define our workding outside of $GOPATH (we're using go modules). -USER gotenberg -WORKDIR /gotenberg/tests - -# Install module dependencies. -COPY --chown=gotenberg:gotenberg go.mod go.sum ./ - -RUN go mod download &&\ - go mod verify - -# Copy our code source. -COPY --chown=gotenberg:gotenberg . . - -ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ] \ No newline at end of file diff --git a/build/tests/docker-entrypoint.sh b/build/tests/docker-entrypoint.sh deleted file mode 100755 index f51b3776..00000000 --- a/build/tests/docker-entrypoint.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -set -xe - -# Make sure the user running the -# tests is the Gotenberg user. -CURRENT_USER=$(whoami) -if [ "$CURRENT_USER" != "gotenberg" ]; then - exit 1 -fi - -# Start Google Chrome headless. -go run test/cmd/chrome.go - -# Run our tests. -if [ "$CODE_COVERAGE" = "1" ]; then - go test -race -coverprofile=coverage.txt -covermode=atomic ./... -else - go test -race -cover ./... -fi \ No newline at end of file diff --git a/build/workspace/Dockerfile b/build/workspace/Dockerfile deleted file mode 100644 index ecf13478..00000000 --- a/build/workspace/Dockerfile +++ /dev/null @@ -1,52 +0,0 @@ -ARG GOLANG_VERSION - -FROM golang:${GOLANG_VERSION}-stretch as golang - -FROM thecodingmachine/gotenberg:base - -# |-------------------------------------------------------------------------- -# | Common libraries -# |-------------------------------------------------------------------------- -# | -# | Libraries used in the build process of this image. -# | - -RUN apt-get update && apt-get install -y --no-install-recommends \ - git \ - g++ \ - gcc \ - libc6-dev \ - make \ - pkg-config \ - && rm -rf /var/lib/apt/lists/* - -# |-------------------------------------------------------------------------- -# | Golang -# |-------------------------------------------------------------------------- -# | -# | Installs Golang. -# | - -COPY --from=golang /usr/local/go /usr/local/go - -ENV GOPATH /gotenberg/go -ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH - -RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" &&\ - chmod -R 777 "$GOPATH" - -# |-------------------------------------------------------------------------- -# | Final touch -# |-------------------------------------------------------------------------- -# | -# | Last instructions of this build. -# | - -# Make sure the Gotenber user is able to -# call the Go binary. -USER gotenberg - -RUN go version &&\ - go env - -USER root \ No newline at end of file diff --git a/cmd/gotenberg/main.go b/cmd/gotenberg/main.go deleted file mode 100644 index f6c0d3fd..00000000 --- a/cmd/gotenberg/main.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net/http" - "os" - "os/signal" - - "github.com/thecodingmachine/gotenberg/internal/app/xhttp" - "github.com/thecodingmachine/gotenberg/internal/pkg/chrome" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// version will be set on build time. -// nolint: gochecknoglobals -var version = "snapshot" - -func main() { - const op string = "main" - config, err := conf.FromEnv() - systemLogger := xlog.New(config.LogLevel(), "system") - if err != nil { - systemLogger.FatalOp(op, err) - } - systemLogger.InfoOpf(op, "Gotenberg %s", version) - systemLogger.DebugOpf(op, "configuration: %+v", config) - if !config.DisableGoogleChrome() { - // start Google Chrome headless. - if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil { - systemLogger.FatalOp(op, err) - } - } - // create our API. - srv := xhttp.New(config) - // run our API in a goroutine so that it doesn't block. - go func() { - systemLogger.InfoOpf(op, "http server started on port '%d'", config.DefaultListenPort()) - if err := srv.Start(fmt.Sprintf(":%d", config.DefaultListenPort())); err != nil { - if errors.Is(err, http.ErrServerClosed) { - systemLogger.FatalOp(op, err) - } - } - }() - quit := make(chan os.Signal, 1) - // we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) - // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. - signal.Notify(quit, os.Interrupt) - // block until we receive our signal. - <-quit - // create a deadline to wait for. - ctx, cancel := xcontext.WithTimeout(systemLogger, 120) - defer cancel() - // doesn't block if no connections, but will otherwise wait - // until the timeout deadline. - systemLogger.InfoOp(op, "shutting down http server...") - if err := srv.Shutdown(ctx); err != nil { - systemLogger.FatalOp(op, err) - } - systemLogger.InfoOp(op, "bye!") - os.Exit(0) -} diff --git a/docs/CNAME b/docs/CNAME deleted file mode 100644 index 4253e4d2..00000000 --- a/docs/CNAME +++ /dev/null @@ -1 +0,0 @@ -gotenberg.dev \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100755 index b2171dd3..00000000 --- a/docs/index.html +++ /dev/null @@ -1,1797 +0,0 @@ - - - - - - - - Gotenberg · A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF. - - - - - - - - - - - - - - - - -
-
-
-
- Gotenberg logo - Gotenberg - A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF. -
-
- -
- - -
- -
-

Introduction

-

Gotenberg is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.

- -
    -
  • HTML and Markdown conversions using Google Chrome headless
  • -
  • Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using unoconv
  • -
  • Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
  • -
  • Easily interact with the API using our Go and PHP libraries
  • -
- -
- -
-

Install

-

Gotenberg is shipped within a Docker image.

- -

Gotenberg should ONLY be used in a trusted network by trusted applications. Do NOT expose Gotenberg to the external world.

- -

You may start it with:

- -
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:6
-
- -
-

The API will be available at http://localhost:3000.

-
- -

The image uses a dedicated non-root user called gotenberg with uid and gid 1001.

- -

If you wish to change those uid and gid, you will have to:

- -
    -
  • clone the project
  • -
  • re-build the image
  • -
  • publish the image in your own Docker registry
  • -
- -

For instance:

- -
$ git clone https://github.com/thecodingmachine/gotenberg.git
-$ make publish GOTENBERG_USER_GID=your_custom_gid GOTENBERG_USER_UID=your_custom_uid DOCKER_REGISTRY=your_registry DOCKER_USER=registry_user DOCKER_PASSWORD=registry_password VERSION=version
-
- -
-

master branch is always up-to-date with the latest version of the API.

-
- -

Docker Compose

- -

You may also add it in your Docker Compose stack:

- -
version: '3'
-
-services:
-
-  # your other services
-
-  gotenberg:
-    image: thecodingmachine/gotenberg:6
-
- -
-

The API will be available under gotenberg:3000 in your Docker Compose network.

-
- -

Kubernetes

- -

It may also be deployed with Kubernetes.

- -

Make sure to provide enough memory and CPU requests (for instance 512Mi and 0.2 CPU).

- -
-

The more resources are granted, the quicker will be the conversions.

-
- -

In the deployment specification of the pod, also specify the uid of the user gotenberg:

- -
securityContext:
-  privileged: false
-  runAsUser: 1001
-
- -

Cloud Run (Google Cloud)

- -

If you’re looking for cost savings, you might be interested by Cloud Run. -However, according to some users, doing asynchronous conversion (with a webhook) might not working.

- -

In the following examples, we will assume your -Gotenberg API is available at http://localhost:3000.

- -
- -
-

Clients

-

We provide clients in various languages for easing the interactions with the API.

- -

Go client

- -
$ go get -u github.com/thecodingmachine/gotenberg-go-client/v7
-
- -

See also the example from the README.

- -

PHP client

- -

Unless your project already has a PSR7 HttpClient, install php-http/guzzle6-adapter:

- -
$ composer require php-http/guzzle6-adapter
-
- -

Then the PHP client:

- -
$ composer require thecodingmachine/gotenberg-php-client
-
- -

See also the example from the README.

- -

Community clients

- - - -
- -
-

Environment variables

-

You may customize the API behaviour thanks to environment variables.

- -

Log level

- -

The API provides structured logging allowing you to have relevant information -about what’s going on.

- -
-

If a TTY is attached, the log entries are displayed in text format with colors, otherwise in JSON format.

-
- -

You may customize the severity of the log entries thanks to the environment variable LOG_LEVEL.

- -

It accepts one of the following severities: "DEBUG", "INFO" (default) and "ERROR".

- -

Default listen port

- -

By default, the API will listen on port 3000.

- -

You may customize this value with the environment variable DEFAULT_LISTEN_PORT.

- -

This environment variable accepts any string that can be turned into a port number.

- -

Root path

- -

By default, the API root path is /.

- -

You may customize this value with the environment variable ROOT_PATH.

- -

This environment variable accepts a string starting and ending with /.

- -

For instance, /gotenberg/ is a valid value while gotenberg is not.

- -
-

This is useful if you wish to do service discovery via URL paths.

-
- -

Disable Google Chrome

- -

In order to save some resources, the Gotenberg image accepts the environment variable DISABLE_GOOGLE_CHROME -for disabling Google Chrome.

- -

It takes the strings "0" or "1" as value where 1 means true

- -
-

If Google Chrome is disabled, the following conversions will not be available anymore: -HTML, URL and Markdown

-
- -

Default Google Chrome rpcc buffer size

- -

When performing a HTML, URL or Markdown conversion, the API might return -a 400 HTTP code with the message increase the Google Chrome rpcc buffer size.

- -

If so, you may increase this buffer size with the environment variable DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE.

- -

It takes a string representation of an int as value (e.g. "1048576" for 1 MB). -The hard limit is 100 MB and is defined by Google Chrome itself.

- -
-

The default Google Chrome rpcc buffer size may also be overridden per request thanks to the form field googleChromeRpccBufferSize. -See the rpcc buffer size section.

-
- -

Google Chrome ignore certificate errors

- -

When performing a URL conversion, Google Chrome will not accept certificate errors.

- -

You may allow insecure connections by setting the GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS environment variable to "1".

- -

You should be careful with this feature and only enable it in your development environment.

- -

Disable LibreOffice (unoconv)

- -

You may also disable LibreOffice (unoconv) with DISABLE_UNOCONV.

- -
-

If LibreOffice (unoconv) is disabled, the following conversion will not be available anymore: -Office

-
- -

Default wait timeout

- -

By default, the API will wait 10 seconds before it considers the conversion to be unsuccessful. -If unsucessful, it returns a 504 HTTP code.

- -

You may customize this timeout thanks to the environment variable DEFAULT_WAIT_TIMEOUT.

- -

It takes a string representation of a float as value (e.g "2.5" for 2.5 seconds).

- -
-

The default timeout may also be overridden per request thanks to the form field waitTimeout. -See the timeout section.

-
- -

Maximum wait timeout

- -

By default, the value of the form field waitTimeout cannot be more than 30 seconds.

- -

You may increase or decrease this limit thanks to the environment variable MAXIMUM_WAIT_TIMEOUT.

- -

It takes a string representation of a float as value (e.g "2.5" for 2.5 seconds).

- -

Default webhook URL timeout

- -

By default, the API will wait 10 seconds before it considers the sending of the resulting PDF to be unsuccessful.

- -
-

See the webhook section.

-
- -

You may customize this timeout thanks to the environment variable DEFAULT_WEBHOOK_URL_TIMEOUT.

- -

It takes a string representation of a float as value (e.g "2.5" for 2.5 seconds).

- -
-

The default timeout may also be overridden per request thanks to the form field webhookURLTimeout. -See the webhook timeout section.

-
- -

Maximum webhook URL timeout

- -

By default, the value of the form field webhookURLTimeout cannot be more than 30 seconds.

- -

You may increase or decrease this limit thanks to the environment variable MAXIMUM_WEBHOOK_URL_TIMEOUT.

- -

It takes a string representation of a float as value (e.g "2.5" for 2.5 seconds).

- -

Maximum wait delay

- -

By default, the value of the form field waitDelay cannot be more than 10 seconds.

- -
-

See the wait delay section.

-
- -

You may increase or decrease this limit thanks to the environment variable MAXIMUM_WAIT_DELAY.

- -

It takes a string representation of a float as value (e.g "2.5" for 2.5 seconds).

- -
- -
-

HTML

-

Gotenberg provides the endpoint /convert/html for HTML conversions.

- -

It accepts POST requests with a multipart/form-data Content-Type.

- -

Basic

- -

The only requirement is to send a file named index.html: it is the file -which will be converted to PDF.

- -

For instance:

- -
<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>My PDF</title>
-  </head>
-  <body>
-    <h1>Hello world!</h1>
-  </body>
-</html>
-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Header and footer

- -

You may also add a header and/or a footer in the resulting PDF. -Respectively, a file named header.html and footer.html.

- -

Each of them has to be a complete HTML document:

- -
<html>
-    <head>
-        <style>
-            body {
-                font-size: 8rem;
-                margin: 4rem auto;
-            }
-        </style>
-    </head>
-    <body>
-        <p>
-            <span class="pageNumber"></span> of <span class="totalPages"></span>
-        </p>
-    </body>
-</html>
-
- -

The following classes allow you to inject printing values:

- -
    -
  • date: formatted print date
  • -
  • title: document title
  • -
  • pageNumber: current page number
  • -
  • totalPage: total pages in the document
  • -
- -

There are some limitations:

- -
    -
  • JavaScript is not executed
  • -
  • external resources are not loaded
  • -
  • the CSS properties are independant of the ones used in the index.html file
  • -
  • footer.html CSS properties override the ones from header.html
  • -
  • only fonts installed in the Docker image are loaded (see the fonts section)
  • -
  • images only work using a base64 encoded source (<img src="data:image/png;base64, iVBORw0K... />)
  • -
  • background-color and color CSS properties require an additional -webkit-print-color-adjust: exact CSS property in order to work
  • -
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form files=@header.html \
-    --form files=@footer.html \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file")
-footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.Header(header)
-req.Footer(footer)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$header = DocumentFactory::makeFromPath('header.html', '/path/to/file');
-$footer = DocumentFactory::makeFromPath('footer.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setHeader($header);
-$request->setFooter($footer);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Assets

- -

You may also send additional files. For instance: images, fonts, stylesheets and so on.

- -

The only requirement is to make sure that their paths -are on the same level as the index.html file.

- -

In other words, this will work:

- -
<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>My PDF</title>
-  </head>
-  <body>
-    <h1>Hello world!</h1>
-    <img src="img.png">
-  </body>
-</html>
-
- -

But this won’t:

- -
<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>My PDF</title>
-  </head>
-  <body>
-    <h1>Hello world!</h1>
-    <img src="/foo/img.png">
-  </body>
-</html>
-
- -

You may also use remote paths for Google fonts, images and so on.

- -
-

If you want to install fonts directly in the Gotenberg Docker image, -see to the fonts section.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form files=@style.css \
-    --form files=@img.png \
-    --form files=@font.woff \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file")
-img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file")
-font, _ := gotenberg.NewDocumentFromPath("font.woff", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.Assets(style, img, font)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$assets = [
-    DocumentFactory::makeFromPath('style.css', '/path/to/file'),
-    DocumentFactory::makeFromPath('img.png', '/path/to/file'),
-    DocumentFactory::makeFromPath('font.woff', '/path/to/file'),
-];
-$request = new HTMLRequest($index);
-$request->setAssets($assets);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Paper size, margins, orientation, scaling

- -

You may also customize the resulting PDF format.

- -

By default, it will be rendered with A4 size, 1 inch margins and portrait orientation and 100% (1.0) page scale.

- -
-

Paper size and margins have to be provided in inches. Same for margins.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form paperWidth=8.27 \
-    --form paperHeight=11.69 \
-    --form marginTop=0 \
-    --form marginBottom=0 \
-    --form marginLeft=0 \
-    --form marginRight=0 \
-    --form landscape=true \
-    --form scale=0.75 \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.PaperSize(gotenberg.A4)
-req.Margins(gotenberg.NoMargins)
-req.Landscape(true)
-req.Scale(0.75)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setPaperSize(Request::A4);
-$request->setMargins(Request::NO_MARGINS);
-$request->setLandscape(true);
-$request->setScale(0.75);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Page ranges

- -

You may specify the page ranges to convert.

- -

The format is the same as the one from the print options -of Google Chrome, e.g. 1-5,8,11-13.

- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form pageRanges='1-3,5' \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.PageRanges("1-3,5")
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setPageRanges('1-3,5');
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Wait delay

- -

In some cases, you may want to wait a certain amount of time to make sure the -page you’re trying to generate is fully rendered. For instance, if your page relies -a lot on JavaScript for rendering.

- -
-

The wait delay is a duration in seconds (e.g 2.5 for 2.5 seconds).

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form waitDelay=5.5 \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.WaitDelay(5.5)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setWaitDelay(5.5);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Rpcc buffer size

- -

The API might return a 400 HTTP code with the message increase the Google Chrome rpcc buffer size.

- -

If so, you may increase this buffer size with a form field named googleChromeRpccBufferSize.

- -

It takes an int as value (e.g. 1048576 for 1 MB). -The hard limit is 100 MB and is defined by Google Chrome itself.

- -
-

You may also define this value globally: see the environment variables section.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form googleChromeRpccBufferSize=1048576 \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.GoogleChromeRpccBufferSize(1048576)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setGoogleChromeRpccBufferSize(1048576);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

URL

-

Gotenberg provides the endpoint /convert/url for remote URL conversions.

- -

It accepts POST requests with a multipart/form-data Content-Type.

- -

Basic

- -

This endpoint does not accept an index.html file nor assets files but a form field -named remoteURL instead. Otherwise, URL conversions work the same as HTML conversions.

- -
-

Attention: when converting a website to PDF, you should remove all margins. -If not, some of the content of the page might be hidden.

- -

Attention: if you try to convert a URL from a Docker Compose service named app (i.e. removeURL = http://app/an/entrypoint), -the resulting PDF will be blank. Make sure to rename your service to avoid this issue.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/url \
-    --header 'Content-Type: multipart/form-data' \
-    --form remoteURL=https://google.com \
-    --form marginTop=0 \
-    --form marginBottom=0 \
-    --form marginLeft=0 \
-    --form marginRight=0 \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-req := gotenberg.NewURLRequest("https://google.com")
-req.Margins(gotenberg.NoMargins)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\URLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$request = new URLRequest('https://google.com');
-$request->setMargins(Request::NO_MARGINS);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Custom HTTP headers

- -

You may send your own HTTP headers to the remoteURL.

- -

For instance, by adding the HTTP header Gotenberg-Remoteurl-Your-Header to your request, -the API will send a request to the remoteURL with the HTTP header Your-Header.

- -
-

Attention: the API uses a canonical format for the HTTP headers: -it transforms the first -letter and any letter following a hyphen to upper case; -the rest are converted to lowercase. For example, the -canonical key for accept-encoding is Accept-Encoding.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/url \
-    --header 'Content-Type: multipart/form-data' \
-    --header 'Gotenberg-Remoteurl-Your-Header: Foo' \
-    --form remoteURL=https://google.com \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-req := gotenberg.NewURLRequest("https://google.com")
-req.AddRemoteURLHTTPHeader("Your-Header", "Foo")
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\URLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$request = new URLRequest('https://google.com');
-$request->addRemoteURLHTTPHeader('Your-Header', 'Foo')
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

Markdown

-

Gotenberg provides the endpoint /convert/markdown for Markdown conversions.

- -

It accepts POST requests with a multipart/form-data Content-Type.

- -

Basic

- -

Markdown conversions work the same as HTML conversions.

- -

Only difference is that you have access to the Go template function toHTML -in the file index.html. This function will convert a given markdown file to HTML.

- -

For instance:

- -
<!doctype html>
-<html lang="en">
-  <head>
-    <meta charset="utf-8">
-    <title>My PDF</title>
-  </head>
-  <body>
-    {{ toHTML .DirPath "file.md" }}
-  </body>
-</html>
-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/markdown \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form files=@file.md \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-markdown, _ := gotenberg.NewDocumentFromPath("file.md", "/path/to/file")
-req := gotenberg.NewMarkdownRequest(index, markdown)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\MarkdownRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$markdowns = [
-    DocumentFactory::makeFromPath('file.md', '/path/to/file'),
-];
-$request = new MarkdownRequest($index, $markdowns);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

Office

-

Gotenberg provides the endpoint /convert/office for Office document conversions.

- -

It accepts POST requests with a multipart/form-data Content-Type.

- -

Basic

- -

You may send one or more Office documents. Following file extensions are accepted:

- -
    -
  • .txt
  • -
  • .rtf
  • -
  • .fodt
  • -
  • .doc
  • -
  • .docx
  • -
  • .odt
  • -
  • .xls
  • -
  • .xlsx
  • -
  • .ods
  • -
  • .ppt
  • -
  • .pptx
  • -
  • .odp
  • -
- -

All files will be merged into a single resulting PDF.

- -
-

Attention: Gotenberg merges the PDF files alphabetically.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/office \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@document.docx \
-    --form files=@document2.docx \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
-doc2, _ := gotenberg.NewDocumentFromPath("document2.docx", "/path/to/file")
-req := gotenberg.NewOfficeRequest(doc, doc2)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\OfficeRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$files = [
-    DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
-    DocumentFactory::makeFromPath('document2.docx', '/path/to/file'),
-];
-$request = new OfficeRequest($files);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Orientation

- -

You may also customize the resulting PDF format.

- -

By default, it will be rendered with portrait orientation.

- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/office \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@document.docx \
-    --form landscape=true \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
-req := gotenberg.NewOfficeRequest(doc)
-req.Landscape(true)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\OfficeRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$files = [
-    DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
-];
-$request = new OfficeRequest($files);
-$request->setLandscape(true);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -

Page ranges

- -

You may specify the page ranges to convert.

- -

The format is the same as the one from the print options -of LibreOffice, e.g. 1-1 or 1-4.

- -
-

Attention: if more than one document, the page ranges will be -applied for each document.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/office \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@document.docx \
-    --form pageRanges='1-3' \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-doc, _ := gotenberg.NewDocumentFromPath("document.docx", "/path/to/file")
-req := gotenberg.NewOfficeRequest(doc)
-req.PageRanges("1-3")
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\OfficeRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$files = [
-    DocumentFactory::makeFromPath('document.docx', '/path/to/file'),
-];
-$request = new OfficeRequest($files);
-$request->setPageRanges('1-3');
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

Merge

-

Gotenberg provides the endpoint /merge for merging PDFs.

- -

It accepts POST requests with a multipart/form-data Content-Type.

- -

Basic

- -

Nothing fancy here: you may send one or more PDF files and the API -will merge them and return the resulting PDF file.

- -
-

Attention: Gotenberg merges the PDF files alphabetically.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/merge \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@file.pdf \
-    --form files=@file2.pdf \
-    -o result.pdf
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-pdf, _ := gotenberg.NewDocumentFromPath("file.pdf", "/path/to/file")
-pdf2, _ := gotenberg.NewDocumentFromPath("file2.pdf", "/path/to/file")
-req := gotenberg.NewMergeRequest(pdf, pdf2)
-dest := "result.pdf"
-c.Store(req, dest)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\MergeRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$files = [
-    DocumentFactory::makeFromPath('file.pdf', '/path/to/file'),
-    DocumentFactory::makeFromPath('file2.pdf', '/path/to/file'),
-];
-$request = new MergeRequest($files);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

Timeout

-

All endpoints accept a form field named waitTimeout.

- -

The API will wait the given seconds before it considers the conversion to be unsucessful. -If unsucessful, it returns a 504 HTTP code.

- -

It takes a float as value (e.g 2.5 for 2.5 seconds).

- -
-

You may also define this value globally: see the environment variables section.

-
- -

Examples

- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form waitTimeout=2.5
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.WaitTimeout(2.5)
-resp, _ := c.Post(req)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setWaitTimeout(2.5);
-$dest = 'result.pdf';
-$client->store($request, $dest);
-
- -
- -
-

Webhook

-

All endpoints accept a form field named webhookURL.

- -

If provided, the API will send the resulting PDF file in a POST request with the application/pdf Content-Type -to given URL.

- -

By doing so, your requests to the API will be over before the conversions are actually done!

- -

Examples

- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form webhookURL='http://myapp.com/webhook/'
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.WebhookURL("http://myapp.com/webhook/")
-resp, _ := c.Post(req)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setWebhookURL('http://myapp.com/webhook/');
-$resp = $client->post($request);
-
- -

Timeout

- -

If a webhookURL is provided, you may also send a form field named webhookURLTimeout.

- -

The API will wait the given seconds before it considers the sending of the resulting PDF to be unsucessful.

- -

It takes a float as value (e.g 2.5 for 2.5 seconds).

- -
-

You may also define this value globally: see the environment variables section.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form webhookURL='http://myapp.com/webhook/' \
-    --form webhookURLTimeout=2.5
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.WebhookURL("http://myapp.com/webhook/")
-req.WebhookURLTimeout(2.5)
-resp, _ := c.Post(req)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setWebhookURL('http://myapp.com/webhook/');
-$request->setWebhookURLTimeout(2.5);
-$resp = $client->post($request);
-
- -

Custom HTTP headers

- -

You may send your own HTTP headers to the webhookURL.

- -

For instance, by adding the HTTP header Gotenberg-Webhookurl-Your-Header to your request, -the API will send a request to the webhookURL with the HTTP header Your-Header.

- -
-

Attention: the API uses a canonical format for the HTTP headers: -it transforms the first -letter and any letter following a hyphen to upper case; -the rest are converted to lowercase. For example, the -canonical key for accept-encoding is Accept-Encoding.

-
- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --header 'Gotenberg-Webhookurl-Your-Header: Foo' \
-    --form files=@index.html \
-    --form webhookURL='http://myapp.com/webhook/'
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.WebhookURL("http://myapp.com/webhook/")
-req.AddWebhookURLHTTPHeader("Your-Header", "Foo")
-resp, _ := c.Post(req)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setWebhookURL('http://myapp.com/webhook/');
-$request->addWebhookURLHTTPHeader('Your-Header', 'Foo');
-$resp = $client->post($request);
-
- -
- -
-

Result filename

-

All endpoints accept a form field named resultFilename.

- -

If provided, the API will return the resulting PDF file with the given filename. -Otherwise a random filename is used.

- -
-

Attention: this feature does not work if the form field webhookURL is given.

-
- -

Examples

- -

cURL

- -
$ curl --request POST \
-    --url http://localhost:3000/convert/html \
-    --header 'Content-Type: multipart/form-data' \
-    --form files=@index.html \
-    --form resultFilename='foo.pdf'
-
- -

Go

- -
import "github.com/thecodingmachine/gotenberg-go-client/v7"
-
-c := &gotenberg.Client{Hostname: "http://localhost:3000"}
-index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file")
-req := gotenberg.NewHTMLRequest(index)
-req.ResultFilename("foo.pdf")
-resp, _ := c.Post(req)
-
- -

PHP

- -
use TheCodingMachine\Gotenberg\Client;
-use TheCodingMachine\Gotenberg\DocumentFactory;
-use TheCodingMachine\Gotenberg\HTMLRequest;
-use TheCodingMachine\Gotenberg\Request;
-
-$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
-$index = DocumentFactory::makeFromPath('index.html', '/path/to/file');
-$request = new HTMLRequest($index);
-$request->setResultFilename('foo.pdf');
-$resp = $client->post($request);
-
- -
- -
-

Scalability

-

The API uses under the hood intricate programs.

- -

Gotenberg tries to abstract as much complexity as possible but it can -only do it to a certain extent.

- -

For instance, Office and Merge endpoints will start respectively as many LibreOffice (unoconv) and PDTk -instances as there are requests. The limitation here is the available memory and CPU usage.

- -

On another hand, for the HTML, URL and Markdown endpoints, the API does only 6 conversions in parallel. -Indeed, Google Chrome misbehaves if there are too many concurrent conversions.

- -

The more concurrent requests, the more 504 HTTP codes the API will return.

- -
-

See our load testing use case for more details about the API behaviour under heavy load.

-
- -

Strategies

- -

Increase timeout

- -

You may increase the conversion timeout. In other words, you accept that a conversion takes more time -if the API is under heavy load.

- -
-

See timeout section.

-
- -

Scaling

- -

The API being stateless, you may scale it as much as you want.

- -

For instance, using the following Docker Compose file:

- -
version: '3'
-
-services:
-
-  # your other services
-
-  gotenberg:
-    image: thecodingmachine/gotenberg:6
-
- -

You may now launch your services using:

- -
$ docker-compose up --scale gotenberg=your_number_of_instances
-
- -

When requesting the Gotenberg service with your client(s), Docker will automatically -redirect a request to a Gotenberg container according to the round-robin strategy.

- -
- -
-

Ping

-

Gotenberg provides the endpoint /ping for checking the API availability with -a simple GET request.

- -

Currently this endpoint does nothing special. A better way to monitor -Gotenberg would be by checking the memory usage.

- -

Also, as the API uses under the hood intricate programs, you should -restart your Gotenberg instances from time to time to ensure a nominal behaviour.

- -
- -
-

Fonts

-

By default, a handful of fonts are installed. Asian characters are also supported out of the box.

- -

If you wish to use more fonts, you will have to create your own image:

- -
FROM thecodingmachine/gotenberg:6
-
-USER root
-
-RUN apt-get -y install yourfonts
-
-USER gotenberg
-
- -
- - - - - -
-
-
- - -
\ No newline at end of file diff --git a/docs/openapi.yaml b/docs/openapi.yaml deleted file mode 100644 index 5e487067..00000000 --- a/docs/openapi.yaml +++ /dev/null @@ -1,569 +0,0 @@ -openapi: 3.0.0 -info: - title: Gotenberg - version: 6.3.1 - license: - name: MIT - url: 'https://github.com/thecodingmachine/gotenberg/blob/master/LICENSE' - contact: - url: 'https://github.com/thecodingmachine/gotenberg' - description: >- - A Docker-powered stateless API for converting HTML, Markdown and Office - documents to PDF. -servers: - - url: 'http://localhost:3000' - description: Local server with the default Docker image and port -paths: - /convert/html: - post: - tags: - - Gotenberg - summary: Convert a given HTML file to PDF - description: >- - Send an HTML file called `index.html` as a multipart form request, and - get the resulting PDF file. You can optionally include `header.html` and - `footer.html` files as part of the request as well. - - - An example `index.html` file can be as follows: - - ```html - - - - - - - My PDF - - -

Hello world!

- - - - ``` - - - You may also add a header and/or a footer in the resulting PDF by - sending a file named `header.html` and `footer.html` respectively. Both - the header and the footer files has to be a complete HTML document. An - example `footer.html` can be as follows: - - ```html - - - - - - -

- of -

- - - - ``` - - - The following classes will allow you to inject printing values in your - document: - - - `date`: Formatted print date. - - - `title`: Document title. - - - `pageNumber`: Current page number. - - - `totalPage`: Total pages in the document. - - - There are some limitations with header and footer files: - - - JavaScript is not executed. - - - External resources are not loaded. - - - The CSS properties are independant of the ones used in the - `index.html` file. - - - `footer.html` CSS properties override the ones from `header.html`. - - - Only fonts installed in the Docker image are loaded (see the fonts - section) - - - Images only work using a `base64` encoded source, e.g. ` - - - My PDF - - - {{ toHTML .DirPath "file.md" }} - - - - ``` - - - Whereas your `file.md` file in the same level would be like: - - ```md - - # Title - - Content - - ``` - - - The API will convert the markdown to HTML and embed it into your - `index.html` file, then render the resulting page. You can include your - own styling and more in your HTML file. - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/MarkdownConvertRequestBody' - description: >- - The request body must have an `index.html` file in the `files` array, - as well as all the referred markdown resources on the same level as - the `index.html` file. - responses: - '200': - $ref: '#/components/responses/SuccessfulPDF' - /convert/office: - post: - tags: - - Gotenberg - summary: Convert an Office document to PDF - description: >- - Send one or more Office documents and get the resulting PDF file by - merging all the files. The following file extensions are accepted: - - - `.txt` - - - `.rtf` - - - `.fodt` - - - `.doc` - - - `.docx` - - - `.odt` - - - `.xls` - - - `.xlsx` - - - `.ods` - - - `.ppt` - - - `.pptx` - - - `.odp` - - - All files will be merged into a single PDF. - - > **Attention:** The files will be merged alphabetically for the - resulting PDF. - - - You may also specify the page ranges to convert from the incoming Office - documents. The expected format is the same as the one from the print - options of LibreOffice, e.g. `1-1` or `1-4`. - - - > **Attention:** if more than one document, the page ranges will be - applied for each document. - requestBody: - content: - multipart/form-data: - schema: - $ref: '#/components/schemas/OfficeConvertRequestBody' - description: >- - The request body must have an `index.html` file in the `files` array, - as well as all the referred markdown resources on the same level as - the `index.html` file. - responses: - '200': - $ref: '#/components/responses/SuccessfulPDF' - content: - application/json: - schema: - type: object - properties: {} - /convert/merge: - post: - tags: - - Gotenberg - summary: Merge multiple PDFs into a single PDF - description: >- - You can send multiple PDF files to this endpoint, the API will merge - them into a single PDF and return the resulting PDF file. - - - > **Attention:** The PDF files will be merged alphabetically. - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - files: - type: array - description: >- - List of HTML files to be converted to PDF. An `index.html` - file is required, and any other resources that are - referenced through the HTML file must be included as well. - All the referenced files must be on the same level as the - `index.html` file. - items: - type: string - format: binary - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with - the given filename. Otherwise a random filename is used. - required: - - files - description: >- - The request body must have an `index.html` file in the `files` array, - as well as all the referred markdown resources on the same level as - the `index.html` file. - responses: - '200': - $ref: '#/components/responses/SuccessfulPDF' - /ping: - get: - tags: - - Gotenberg - summary: Your GET endpoint - responses: - '200': - description: The API is working fine. - operationId: '' - description: >- - A simple endpoint to use as a healthcheck for the API availability with - a simple request. -components: - schemas: - HTMLConvertRequestBody: - title: HTML Conversion Request Body - type: object - properties: - files: - type: array - description: >- - List of HTML files to be converted to PDF. An `index.html` file is - required, and any other resources that are referenced through the - HTML file must be included as well. All the referenced files must be - on the same level as the `index.html` file. - items: - type: string - format: binary - marginTop: - type: number - example: 0 - default: 1 - description: Top margin for the page in inches. - marginBottom: - type: number - example: 0 - default: 1 - description: Bottom margin for the page in inches. - marginLeft: - type: number - example: 0 - default: 1 - description: Left margin for the page in inches. - marginRight: - type: number - example: 0 - default: 1 - description: Right margin for the page in inches. - paperWidth: - type: number - example: 8.27 - description: >- - Paper width to be used while rendering the PDF. The default page - size is A4. - paperHeight: - type: number - example: 11.69 - description: >- - Paper height to be used while rendering the PDF. The default page - size is A4. - landscape: - type: boolean - example: true - default: false - description: >- - The default orientation for rendering the page is "portrait" mode. - By sending "landscape" parameter, you can ask the output to be - landscape. - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with the - given filename. Otherwise a random filename is used. - required: - - files - MarkdownConvertRequestBody: - title: Markdown Conversion Request Body - type: object - properties: - files: - type: array - description: >- - List of HTML files to be converted to PDF. An `index.html` file is - required, and any other resources that are referenced through the - HTML file must be included as well. All the referenced files must be - on the same level as the `index.html` file. - items: - type: string - format: binary - marginTop: - type: number - example: 0 - default: 1 - description: Top margin for the page in inches. - marginBottom: - type: number - example: 0 - default: 1 - description: Bottom margin for the page in inches. - marginLeft: - type: number - example: 0 - default: 1 - description: Left margin for the page in inches. - marginRight: - type: number - example: 0 - default: 1 - description: Right margin for the page in inches. - paperWidth: - type: number - example: 8.27 - description: >- - Paper width to be used while rendering the PDF. The default page - size is A4. - paperHeight: - type: number - example: 11.69 - description: >- - Paper height to be used while rendering the PDF. The default page - size is A4. - landscape: - type: boolean - example: true - default: false - description: >- - The default orientation for rendering the page is "portrait" mode. - By sending "landscape" parameter, you can ask the output to be - landscape. - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with the - given filename. Otherwise a random filename is used. - required: - - files - URLConvertRequestBody: - title: URL Conversion Request Body - type: object - properties: - remoteURL: - type: string - example: 'https://google.com' - marginTop: - type: number - example: 0 - default: 1 - description: Top margin for the page in inches. - marginBottom: - type: number - example: 0 - default: 1 - description: Bottom margin for the page in inches. - marginLeft: - type: number - example: 0 - default: 1 - description: Left margin for the page in inches. - marginRight: - type: number - example: 0 - default: 1 - description: Right margin for the page in inches. - paperWidth: - type: number - example: 8.27 - description: >- - Paper width to be used while rendering the PDF. The default page - size is A4. - paperHeight: - type: number - example: 11.69 - description: >- - Paper height to be used while rendering the PDF. The default page - size is A4. - landscape: - type: boolean - example: true - default: false - description: >- - The default orientation for rendering the page is "portrait" mode. - By sending "landscape" parameter, you can ask the output to be - landscape. - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with the - given filename. Otherwise a random filename is used. - required: - - remoteURL - OfficeConvertRequestBody: - title: Office Conversion Request Body - type: object - properties: - files: - type: array - description: >- - List of HTML files to be converted to PDF. An `index.html` file is - required, and any other resources that are referenced through the - HTML file must be included as well. All the referenced files must be - on the same level as the `index.html` file. - items: - type: string - format: binary - pageRanges: - type: string - example: 1-3 - description: >- - The page ranges to be converted to PDF for the incoming Office - documents. **If there are multiple files sent to the API, this page - range will apply to all of the documents**. - landscape: - type: boolean - example: true - default: false - description: >- - The default orientation for rendering the page is "portrait" mode. - By sending "landscape" parameter, you can ask the output to be - landscape. - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with the - given filename. Otherwise a random filename is used. - required: - - files - MergeFilesRequestBody: - title: Merge Files Request Body - type: object - properties: - files: - type: array - description: >- - List of HTML files to be converted to PDF. An `index.html` file is - required, and any other resources that are referenced through the - HTML file must be included as well. All the referenced files must be - on the same level as the `index.html` file. - items: - type: string - format: binary - resultFilename: - type: string - example: output.pdf - description: >- - If provided, the API will return the resulting PDF file with the - given filename. Otherwise a random filename is used. - required: - - files - securitySchemes: {} - responses: - SuccessfulPDF: - description: Resulting PDF file from the conversion. - content: - application/pdf: - schema: - type: string - format: binary diff --git a/docs/theme/gotenberg/css/index.css b/docs/theme/gotenberg/css/index.css deleted file mode 100644 index f92e503e..00000000 --- a/docs/theme/gotenberg/css/index.css +++ /dev/null @@ -1,342 +0,0 @@ -:root { - --ease: cubic-bezier(.82, 0, .12, 1); - --width: 800px; - --header-height: 400px; - - --tracking: 0.05rem; - --tracking-medium: 0.5rem; - --tracking-large: 0.8rem; - - --dark: #000; - --blue: #33A9FF; - --light-gray: #fafafa; - - --bg: #fff; - --fg: #868E96; - --fg-dark: #212529; - - --selection-bg: var(--blue); - --selection-fg: white; -} - -* { - box-sizing: border-box; -} - -html, body { - font-family: -apple-system, BlinkMacSystemFont, - "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", - "Droid Sans", "Helvetica Neue", sans-serif; - -webkit-font-smoothing: antialiased; - font-size: 16px; - font-weight: 300; - background: var(--bg); - color: var(--fg-dark); - line-height: 1; - margin: 0; - padding: 0; -} - -::selection { - background: var(--selection-bg); - color: var(--selection-fg); -} - -h1, h2, h3, h4 { - margin-top: 75px; - margin-bottom: 0; - font-size: 1.2rem; - font-weight: 600; - line-height: 1.5rem; - color: var(--fg-dark); -} - -h1 { - margin-top: 0; - font-size: 1.5rem; -} - -h1 + p { - font-size: 1.5rem; - line-height: 1.6; -} - -h3 { - font-size: 1.0rem; - font-style: italic; -} - -a { - color: inherit; - text-decoration: none; -} - -p { - margin: 25px 0; - line-height: 1.6; - color: var(--fg-color-light); -} - -li strong, -p strong { - color: var(--fg-dark); - font-weight: 500; -} - -li a, -p a { - color: var(--fg-dark); - font-weight: 400; - padding-bottom: 3px; - border-bottom: 1px dotted #ddd; -} - -li a:hover, -p a:hover { - color: var(--blue); - border-bottom: none; -} - -p a:hover { - border-bottom-color: var(--color); -} - -ul { - margin: 50px 0 50px 30px; - padding: 0; -} - -ul ul { - margin: 10px 0 10px 30px; -} - -ul li { - margin: 5px 0; - color: var(--fg-color-light); - line-height: 1.5em; -} - -ul li strong { - color: var(--fg-color); - font-weight: 500; -} - -pre { - background: var(--light-gray); - color: var(--dark); - padding: 30px; - border-radius: 2px; - overflow: auto; - font: "Source Code Pro", Menlo, monospace; - font-size: .8em; - line-height: 1.5em; -} - -blockquote { - margin: 0; - padding: 0 1em; - color: var(--fg-dark); - border-left: 0.25em solid var(--dark); -} - -li > code, -p > code { - border: 1px solid #DEE2E6; - font-size: 0.75rem; - padding: 3px 10px; - border-radius: 3px; - white-space: nowrap; - font-weight: 600; - font-family: inherit; -} - -details > summary { - cursor: pointer; - outline: none; - user-select: none; -} - -details > p { - border-left: 3px solid var(--dark); - padding-left: 15px; -} - -.Wrapper { - display: flex; - justify-content: center; -} - -.Container { - width: var(--width); - display: flex; - flex-direction: column; -} - -.Content-wrapper { - display: flex; -} - -.Header { - display: flex; - justify-content: center; - align-items: center; - height: var(--header-height); -} - -.Sidebar { - flex: 1 1 auto; -} - -.Content { - width: 75%; -} - -.Content img { - max-width: 100%; -} - -.Menu { - position: -webkit-sticky; - position: sticky; - top: 50px; - color: var(--fg-dark); -} - -.Menu > .item { - padding-bottom: 15px; -} - -.Menu > .item > a { - position: relative; - user-select: none; - font-weight: 400; - transition: color 200ms; - color: var(--fg); -} - -.Menu > .item > a.active { - color: var(--fg-dark); -} - -.Menu > .item > a:before { - content: ""; - position: absolute; - width: 100%; - height: 1px; - bottom: -5px; - left: 0; - background-color: var(--fg-dark); - visibility: hidden; - transform: scaleX(0); - transform-origin: left center; - transition: all 250ms var(--ease); -} - -.Menu > .item > a:hover { - color: var(--fg-dark); -} - -.Menu > .item > a:hover:before { - visibility: visible; - transform: scaleX(1); -} - -.Page { - margin-top: 100px; - padding-top: 50px; -} - -.Page:first-child { - margin-top: 0; - padding-top: 0; -} - -.Title { - margin: 5px 0; - line-height: 2.2em; -} - -.Title.center { - margin-left: auto; - margin-right: auto; - text-align: center; - max-width: 500px; -} - -.Title.margin { - margin-bottom: 80px; -} - -.Title > span { - display: block; -} - -.Title .subtext { - color: var(--fg-color-light); - font-size: 0.8rem; - text-transform: uppercase; - display: none; -} - -.Title .text { - letter-spacing: var(--tracking); - text-transform: uppercase; - font-weight: bold; - font-size: 16px; -} - -.Title.small .text { - font-size: 14px; - text-transform: none; - letter-spacing: normal; - line-height: 2rem; -} - -.Anchor { - position: relative; - margin-left: -14px; - opacity: 0.15; - display: inline-block; - width: 14px; - height: 14px; -} - -.Anchor svg { - position: absolute; - right: 5px; - top: 0; -} - -.Anchor svg path { - pointer-events: all -} - -.Anchor:hover { - opacity: 1; -} - -.Footer { - height: 100px; -} - -@media screen and (max-width: 850px) { - html, body { - --header-height: 300px; - --width: 80%; - font-size: 14px; - } - - .Content-wrapper { - display: flex; - flex-direction: column; - } - - .Sidebar { - margin-bottom: 30px; - } - - .Content { - width: 100%; - } -} diff --git a/docs/theme/gotenberg/img/apple-touch-icon-114x114.png b/docs/theme/gotenberg/img/apple-touch-icon-114x114.png deleted file mode 100644 index 19944d0b..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-114x114.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-120x120.png b/docs/theme/gotenberg/img/apple-touch-icon-120x120.png deleted file mode 100644 index 51e152dd..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-120x120.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-144x144.png b/docs/theme/gotenberg/img/apple-touch-icon-144x144.png deleted file mode 100644 index f9bb3fa2..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-144x144.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-152x152.png b/docs/theme/gotenberg/img/apple-touch-icon-152x152.png deleted file mode 100644 index f2963741..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-152x152.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-57x57.png b/docs/theme/gotenberg/img/apple-touch-icon-57x57.png deleted file mode 100644 index 5d117aa4..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-57x57.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-60x60.png b/docs/theme/gotenberg/img/apple-touch-icon-60x60.png deleted file mode 100644 index d23bac31..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-60x60.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-72x72.png b/docs/theme/gotenberg/img/apple-touch-icon-72x72.png deleted file mode 100644 index 0dbb5112..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-72x72.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/apple-touch-icon-76x76.png b/docs/theme/gotenberg/img/apple-touch-icon-76x76.png deleted file mode 100644 index b52da04e..00000000 Binary files a/docs/theme/gotenberg/img/apple-touch-icon-76x76.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon-128.png b/docs/theme/gotenberg/img/favicon-128.png deleted file mode 100644 index 30b6905c..00000000 Binary files a/docs/theme/gotenberg/img/favicon-128.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon-16x16.png b/docs/theme/gotenberg/img/favicon-16x16.png deleted file mode 100644 index d0fd1582..00000000 Binary files a/docs/theme/gotenberg/img/favicon-16x16.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon-196x196.png b/docs/theme/gotenberg/img/favicon-196x196.png deleted file mode 100644 index 87b8fd3d..00000000 Binary files a/docs/theme/gotenberg/img/favicon-196x196.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon-32x32.png b/docs/theme/gotenberg/img/favicon-32x32.png deleted file mode 100644 index dcc1ea39..00000000 Binary files a/docs/theme/gotenberg/img/favicon-32x32.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon-96x96.png b/docs/theme/gotenberg/img/favicon-96x96.png deleted file mode 100644 index 2f88c3cf..00000000 Binary files a/docs/theme/gotenberg/img/favicon-96x96.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/favicon.ico b/docs/theme/gotenberg/img/favicon.ico deleted file mode 100644 index f98c6258..00000000 Binary files a/docs/theme/gotenberg/img/favicon.ico and /dev/null differ diff --git a/docs/theme/gotenberg/img/mstile-144x144.png b/docs/theme/gotenberg/img/mstile-144x144.png deleted file mode 100644 index f9bb3fa2..00000000 Binary files a/docs/theme/gotenberg/img/mstile-144x144.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/mstile-150x150.png b/docs/theme/gotenberg/img/mstile-150x150.png deleted file mode 100644 index 89ee85f6..00000000 Binary files a/docs/theme/gotenberg/img/mstile-150x150.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/mstile-310x150.png b/docs/theme/gotenberg/img/mstile-310x150.png deleted file mode 100644 index 43d4c5c0..00000000 Binary files a/docs/theme/gotenberg/img/mstile-310x150.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/mstile-310x310.png b/docs/theme/gotenberg/img/mstile-310x310.png deleted file mode 100644 index a1fb31e0..00000000 Binary files a/docs/theme/gotenberg/img/mstile-310x310.png and /dev/null differ diff --git a/docs/theme/gotenberg/img/mstile-70x70.png b/docs/theme/gotenberg/img/mstile-70x70.png deleted file mode 100644 index 30b6905c..00000000 Binary files a/docs/theme/gotenberg/img/mstile-70x70.png and /dev/null differ diff --git a/docs/theme/gotenberg/js/index.js b/docs/theme/gotenberg/js/index.js deleted file mode 100644 index 54804bf8..00000000 --- a/docs/theme/gotenberg/js/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Setup. - */ - -const items = document.querySelectorAll('.Page') -const links = document.querySelectorAll('.Menu a') - -/** - * Check if `el` is out out of view. - */ - -function isBelowScroll(el) { - return el.getBoundingClientRect().bottom > 0 -} - -/** - * Activate item `i`. - */ - -function activateItem(i) { - links.forEach(e => e.classList.remove('active')) - links[i].classList.add('active') -} - -/** - * Activate the correct menu item for the - * contents in the viewport. - */ - -function activate() { - let i = 0 - - for (; i < items.length; i++) { - if (isBelowScroll(items[i])) { - break - } - } - - activateItem(i) -} - -/** - * Activate scroll spy thingy. - */ - -window.addEventListener('scroll', e => activate()) diff --git a/docs/theme/gotenberg/views/index.html b/docs/theme/gotenberg/views/index.html deleted file mode 100644 index f5fb7a3b..00000000 --- a/docs/theme/gotenberg/views/index.html +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - {{.Title}} · {{.Subtitle}} - - - - - - - - - - - - - - - - -
-
-
-
- Gotenberg logo - {{.Title}} - {{.Subtitle}} -
-
- -
- - -
- {{range .Pages}} -
-

{{.Title}}

- {{.Content}} -
- {{end}} - - -
-
-
- - diff --git a/go.mod b/go.mod deleted file mode 100644 index f048f7d6..00000000 --- a/go.mod +++ /dev/null @@ -1,25 +0,0 @@ -module github.com/thecodingmachine/gotenberg - -go 1.14 - -require ( - github.com/dustin/go-humanize v1.0.0 - github.com/kr/text v0.2.0 // indirect - github.com/labstack/echo/v4 v4.3.0 - github.com/labstack/gommon v0.3.0 - github.com/mafredri/cdp v0.31.0 - github.com/mattn/go-isatty v0.0.12 - github.com/microcosm-cc/bluemonday v1.0.9 - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 - github.com/russross/blackfriday/v2 v2.1.0 - github.com/sirupsen/logrus v1.8.1 - github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect - golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect - golang.org/x/sync v0.0.0-20210220032951-036812b2e83c - golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect - golang.org/x/text v0.3.6 - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 9471aa0b..00000000 --- a/go.sum +++ /dev/null @@ -1,115 +0,0 @@ -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.3.0 h1:DCP6cbtT+Zu++K6evHOJzSgA2115cPMuCx0xg55q1EQ= -github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8= -github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/mafredri/cdp v0.31.0 h1:Vd+uCnvBWYsitQRuB/Oxx7S83wfx/ZpeDa4JpSclI6s= -github.com/mafredri/cdp v0.31.0/go.mod h1:YTCwLXkZSa18SGSIxCPMOGZcUJODZSNlAhiMqbyxWJg= -github.com/mafredri/go-lint v0.0.0-20180911205320-920981dfc79e/go.mod h1:k/zdyxI3q6dup24o8xpYjJKTCf2F7rfxLp6w/efTiWs= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/microcosm-cc/bluemonday v1.0.9 h1:dpCwruVKoyrULicJwhuY76jB+nIxRVKv/e248Vx/BXg= -github.com/microcosm-cc/bluemonday v1.0.9/go.mod h1:B2riunDr9benLHghZB7hjIgdwSUzzs0pjCxFrWYEZFU= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2 h1:JhzVVoYvbOACxoUmOs6V/G4D5nPVUW73rKvXxP4XUJc= -github.com/phayes/freeport v0.0.0-20180830031419-95f893ade6f2/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= -github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= -github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= -golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210319071255-635bc2c9138d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 h1:hZR0X1kPW+nwyJ9xRxqZk1vx5RUObAPBdKVvXPDUH/E= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/xhttp/doc.go b/internal/app/xhttp/doc.go deleted file mode 100644 index 62383c98..00000000 --- a/internal/app/xhttp/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package xhttp defines our own implementation -// of echo.Echo. -package xhttp diff --git a/internal/app/xhttp/handler.go b/internal/app/xhttp/handler.go deleted file mode 100644 index dbf0e7b6..00000000 --- a/internal/app/xhttp/handler.go +++ /dev/null @@ -1,383 +0,0 @@ -package xhttp - -import ( - "fmt" - "net/http" - "os" - - "github.com/labstack/echo/v4" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/chrome" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" -) - -func pingEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "ping") -} - -func mergeEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "merge") -} - -func htmlEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "convert/html") -} - -func urlEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "convert/url") -} - -func markdownEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "convert/markdown") -} - -func officeEndpoint(config conf.Config) string { - return fmt.Sprintf("%s%s", config.RootPath(), "convert/office") -} - -func isMultipartFormDataEndpoint(config conf.Config, path string) bool { - var multipartFormDataEndpoints []string - multipartFormDataEndpoints = append(multipartFormDataEndpoints, mergeEndpoint(config)) - if !config.DisableGoogleChrome() { - multipartFormDataEndpoints = append( - multipartFormDataEndpoints, - htmlEndpoint(config), - urlEndpoint(config), - markdownEndpoint(config), - ) - } - if !config.DisableUnoconv() { - multipartFormDataEndpoints = append( - multipartFormDataEndpoints, - officeEndpoint(config), - ) - } - for _, endpoint := range multipartFormDataEndpoints { - if endpoint == path { - return true - } - } - return false -} - -// pingHandler is the handler for healthcheck. -func pingHandler(c echo.Context) error { - const op string = "xhttp.pingHandler" - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - config := ctx.Config() - - logger.DebugOp(op, "handling ping request...") - - if !config.DisableGoogleChrome() { - _, err := chrome.IsViable(logger) - if err != nil { - return xerror.New(op, err) - } - } - - return nil -} - -// mergeHandler is the handler for merging -// PDF files. -func mergeHandler(c echo.Context) error { - const op string = "xhttp.mergeHandler" - resolver := func() error { - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - logger.DebugOp(op, "handling merge request...") - r := ctx.MustResource() - opts, err := mergePrinterOptions(r, ctx.Config()) - if err != nil { - return xerror.New(op, err) - } - fpaths, err := r.Fpaths(".pdf") - if err != nil { - return err - } - p := printer.NewMergePrinter(logger, fpaths, opts) - return convert(ctx, p) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// htmlHandler is the handler for converting -// HTML to PDF. -func htmlHandler(c echo.Context) error { - const op string = "xhttp.htmlHandler" - resolver := func() error { - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - logger.DebugOp(op, "handling HTML request...") - r := ctx.MustResource() - opts, err := chromePrinterOptions(r, ctx.Config()) - if err != nil { - return err - } - fpath, err := r.Fpath("index.html") - if err != nil { - return err - } - p := printer.NewHTMLPrinter(logger, fpath, opts) - return convert(ctx, p) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// urlHandler is the handler for converting -// a URL to PDF. -func urlHandler(c echo.Context) error { - const op string = "xhttp.urlHandler" - resolver := func() error { - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - logger.DebugOp(op, "handling URL request...") - r := ctx.MustResource() - opts, err := chromePrinterOptions(r, ctx.Config()) - if err != nil { - return err - } - opts.CustomHTTPHeaders = resource.RemoteURLCustomHTTPHeaders(r) - if !r.HasArg(resource.RemoteURLArgKey) { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' not found or empty", resource.RemoteURLArgKey), - nil, - ) - } - remoteURL, err := r.StringArg(resource.RemoteURLArgKey, "") - if err != nil { - return err - } - p := printer.NewURLPrinter(logger, remoteURL, opts) - return convert(ctx, p) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// markdownHandler is the handler for converting -// Markdown to PDF. -func markdownHandler(c echo.Context) error { - const op string = "xhttp.markdownHandler" - resolver := func() error { - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - logger.DebugOp(op, "handling Markdown request...") - r := ctx.MustResource() - opts, err := chromePrinterOptions(r, ctx.Config()) - if err != nil { - return err - } - fpath, err := r.Fpath("index.html") - if err != nil { - return err - } - p, err := printer.NewMarkdownPrinter(logger, fpath, opts) - if err != nil { - return err - } - return convert(ctx, p) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// officeHandler is the handler for converting -// Office documents to PDF. -func officeHandler(c echo.Context) error { - const op string = "xhttp.officeHandler" - resolver := func() error { - ctx := context.MustCastFromEchoContext(c) - logger := ctx.XLogger() - logger.DebugOp(op, "handling Office request...") - r := ctx.MustResource() - opts, err := officePrinterOptions(r, ctx.Config()) - if err != nil { - return err - } - fpaths, err := r.Fpaths( - ".txt", - ".rtf", - ".fodt", - ".doc", - ".docx", - ".odt", - ".xls", - ".xlsx", - ".ods", - ".ppt", - ".pptx", - ".odp", - ) - if err != nil { - return err - } - p := printer.NewOfficePrinter(logger, fpaths, opts) - return convert(ctx, p) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func convert(ctx context.Context, p printer.Printer) error { - const op string = "xhttp.convert" - resolver := func() error { - logger := ctx.XLogger() - r := ctx.MustResource() - baseFilename := xrand.Get() - filename := fmt.Sprintf("%s.pdf", baseFilename) - fpath := fmt.Sprintf("%s/%s", r.DirPath(), filename) - // if no webhook URL given, run conversion - // and directly return the resulting PDF file - // or an error. - if !r.HasArg(resource.WebhookURLArgKey) { - logger.DebugOpf(op, "no '%s' found, converting synchronously", resource.WebhookURLArgKey) - return convertSync(ctx, p, filename, fpath) - } - // as a webhook URL has been given, we - // run the following lines in a goroutine so that - // it doesn't block. - logger.DebugOpf(op, "'%s' found, converting asynchronously", resource.WebhookURLArgKey) - return convertAsync(ctx, p, filename, fpath) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func convertSync(ctx context.Context, p printer.Printer, filename, fpath string) error { - const op = "xhttp.convertSync" - resolver := func() error { - logger := ctx.XLogger() - r := ctx.MustResource() - - if err := p.Print(fpath); err != nil { - return err - } - if !r.HasArg(resource.ResultFilenameArgKey) { - logger.DebugOpf( - op, - "no '%s' found, using generated filename '%s'", - resource.RemoteURLArgKey, - filename, - ) - if err := ctx.Attachment(fpath, filename); err != nil { - return err - } - return nil - } - logger.DebugOpf( - op, - "'%s' found, so not using generated filename", - resource.ResultFilenameArgKey, - ) - filename, err := r.StringArg(resource.ResultFilenameArgKey, filename) - if err != nil { - return err - } - if err := ctx.Attachment(fpath, filename); err != nil { - return err - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func convertAsync(ctx context.Context, p printer.Printer, filename, fpath string) error { - const op = "xhttp.convertAsync" - logger := ctx.XLogger() - r := ctx.MustResource() - webhookURL, err := r.StringArg(resource.WebhookURLArgKey, "") - if err != nil { - return xerror.New(op, err) - } - webhookURLTimeout, err := resource.WebhookURLTimeoutArg(r, ctx.Config()) - if err != nil { - return xerror.New(op, err) - } - go func() { - defer r.Close() - if err := p.Print(fpath); err != nil { - xerr := xerror.New(op, err) - logger.ErrorOp(xerror.Op(xerr), xerr) - return - } - f, err := os.Open(fpath) - if err != nil { - xerr := xerror.New(op, err) - logger.ErrorOp(xerror.Op(xerr), xerr) - return - } - defer f.Close() - logger.DebugOpf( - op, - "preparing to send result file '%s' to '%s'...", - filename, - webhookURL, - ) - httpClient := &http.Client{ - Timeout: xtime.Duration(webhookURLTimeout), - } - req, err := http.NewRequest(http.MethodPost, webhookURL, f) - if err != nil { - xerr := xerror.New(op, err) - logger.ErrorOp(xerror.Op(xerr), xerr) - return - } - req.Header.Set(echo.HeaderContentType, "application/pdf") - // set custom headers (if any). - customHTTPHeaders := resource.WebhookURLCustomHTTPHeaders(r) - if len(customHTTPHeaders) > 0 { - for key, value := range customHTTPHeaders { - req.Header.Set(key, value) - logger.DebugOpf(op, "set '%s' to custom HTTP header '%s'", value, key) - } - } else { - logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...") - } - // send the result file. - logger.DebugOpf( - op, - "sending result file '%s' to '%s'...", - filename, - webhookURL, - ) - resp, err := httpClient.Do(req) /* #nosec */ - if err != nil { - xerr := xerror.New(op, err) - logger.ErrorOp(xerror.Op(xerr), xerr) - return - } - defer resp.Body.Close() - logger.DebugOpf( - op, - "result file '%s' sent to '%s'", - filename, - webhookURL, - ) - }() - return nil -} diff --git a/internal/app/xhttp/handler_test.go b/internal/app/xhttp/handler_test.go deleted file mode 100644 index 7cba003e..00000000 --- a/internal/app/xhttp/handler_test.go +++ /dev/null @@ -1,635 +0,0 @@ -package xhttp - -import ( - "errors" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestPingHandler(t *testing.T) { - // should return 200. - config := conf.DefaultConfig() - srv := New(config) - endpoint := pingEndpoint(config) - req := httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodPost, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) -} - -func TestMergeHandler(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - endpoint := mergeEndpoint(config) - // should return 200. - body, contentType := test.MergeMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) - // should return 415 as Content-Type is wrong. - body, _ = test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) - // should return 400 as "waitTimeout" form field - // value is < 0. - body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is is > config.MaximumWaitTimeout(). - body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is invalid. - body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 504. - body, contentType = test.MergeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) -} - -func TestHTMLHandler(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - endpoint := htmlEndpoint(config) - // should return 200. - body, contentType := test.HTMLMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) - // should return 415 as Content-Type is wrong. - body, _ = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) - // should return 400 as "waitTimeout" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is is > config.MaximumWaitTimeout(). - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 504. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) - // should return 400 as "waitDelay" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is is > config.MaximumWaitDelay(). - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "landscape" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is < 0. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is is > config.MaximumGoogleChromeRpccBufferSize(). - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is invalid. - body, contentType = test.HTMLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) -} - -func TestURLHandler(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - endpoint := urlEndpoint(config) - // should return 200. - body, contentType := test.URLMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) - // should return 415 as Content-Type is wrong. - body, _ = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) - // should return 400 as "waitTimeout" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is is > config.MaximumWaitTimeout(). - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 504. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) - // should return 400 as "waitDelay" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is is > config.MaximumWaitDelay(). - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "landscape" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is < 0. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is is > config.MaximumGoogleChromeRpccBufferSize(). - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is invalid. - body, contentType = test.URLMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) -} - -func TestMarkdownHandler(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - endpoint := markdownEndpoint(config) - // should return 200. - body, contentType := test.MarkdownMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) - // should return 415 as Content-Type is wrong. - body, _ = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) - // should return 400 as "waitTimeout" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is is > config.MaximumWaitTimeout(). - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 504. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) - // should return 400 as "waitDelay" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is is > config.MaximumWaitDelay(). - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitDelay" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.WaitDelayArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperWidth" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperWidthArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "paperHeight" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.PaperHeightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginTop" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginTopArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginBottom" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginBottomArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginLeft" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginLeftArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "marginRight" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.MarginRightArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "landscape" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is < 0. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is is > config.MaximumGoogleChromeRpccBufferSize(). - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "104857601"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "googleChromeRpccBufferSize" form field - // value is invalid. - body, contentType = test.MarkdownMultipartForm(t, map[string]string{string(resource.GoogleChromeRpccBufferSizeArgKey): "not an int"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) -} - -func TestOfficeHandler(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - endpoint := officeEndpoint(config) - // should return 200. - // note: increased the wait timeout as it happens to timeout from time to time. - body, contentType := test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "30"}) - req := httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // should return 405 as Method is wrong. - req = httptest.NewRequest(http.MethodGet, endpoint, nil) - test.AssertStatusCode(t, http.StatusMethodNotAllowed, srv, req) - // should return 415 as Content-Type is wrong. - body, _ = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - test.AssertStatusCode(t, http.StatusUnsupportedMediaType, srv, req) - // should return 400 as "waitTimeout" form field - // value is < 0. - body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "-1"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is is > config.MaximumWaitTimeout(). - body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "31"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 400 as "waitTimeout" form field - // value is invalid. - body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "not a float"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) - // should return 504. - body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.WaitTimeoutArgKey): "0"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusGatewayTimeout, srv, req) - // should return 400 as "landscape" form field - // value is invalid. - body, contentType = test.OfficeMultipartForm(t, map[string]string{string(resource.LandscapeArgKey): "not a boolean"}) - req = httptest.NewRequest(http.MethodPost, endpoint, body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusBadRequest, srv, req) -} - -func TestWebhook(t *testing.T) { - customHeaderRealKey := http.CanonicalHeaderKey("MyCustomHeader") - customHeaderKey := fmt.Sprintf("%s%s", resource.WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderRealKey) - customHeaderValue := "foo" - status := make(chan error, 2) - rcv := echo.New() - rcv.POST("/foo", func(c echo.Context) error { - if c.Request().Header.Get(echo.HeaderContentType) != "application/pdf" { - status <- fmt.Errorf("wrong Content-type: got '%s' want '%s'", c.Request().Header.Get(echo.HeaderContentType), "application/pdf") - return nil - } - if c.Request().Header.Get(customHeaderRealKey) != customHeaderValue { - status <- fmt.Errorf("wrong '%s': got '%s' want '%s'", customHeaderRealKey, c.Request().Header.Get(customHeaderRealKey), customHeaderValue) - return nil - } - body, err := ioutil.ReadAll(c.Request().Body) - if err != nil { - status <- err - return nil - } - if body == nil || len(body) == 0 { - status <- errors.New("empty body") - return nil - } - status <- nil - return nil - }) - go func() { - rcv.Start(":3001") - }() - config := conf.DefaultConfig() - srv := New(config) - // our custom server should receive the PDF. - body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.WebhookURLArgKey): "http://localhost:3001/foo"}) - req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - req.Header.Set(customHeaderKey, customHeaderValue) - test.AssertStatusCode(t, http.StatusOK, srv, req) - err := <-status - assert.NoError(t, err) -} - -func TestResultFilename(t *testing.T) { - config := conf.DefaultConfig() - srv := New(config) - body, contentType := test.MergeMultipartForm(t, map[string]string{string(resource.ResultFilenameArgKey): "foo.pdf"}) - req := httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - rec := httptest.NewRecorder() - srv.ServeHTTP(rec, req) - assert.Equal(t, "attachment; filename=\"foo.pdf\"", rec.Header().Get(echo.HeaderContentDisposition)) -} diff --git a/internal/app/xhttp/middleware.go b/internal/app/xhttp/middleware.go deleted file mode 100644 index 4049b9ac..00000000 --- a/internal/app/xhttp/middleware.go +++ /dev/null @@ -1,153 +0,0 @@ -package xhttp - -import ( - "net/http" - "strings" - - "github.com/labstack/echo/v4" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/context" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" -) - -// contextMiddleware extends the default echo.Context with -// our custom context.Context. -func contextMiddleware(config conf.Config) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - // generate a unique identifier for the request. - trace := xrand.Get() - // create the logger for this request using - // the previous identifier as trace. - logger := xlog.New(config.LogLevel(), trace) - // extend the current echo context with our custom - // context. - ctx := context.New(c, logger, config) - // if it's not a multipart/form-data request, - // there is no need to create a Resource. - if !isMultipartFormDataEndpoint(config, ctx.Path()) { - // validate method for healthcheck endpoint. - if ctx.Path() == pingEndpoint(config) && ctx.Request().Method != http.MethodGet { - err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed)) - return ctx.LogRequestResult(err, false) - } - return next(ctx) - } - // validate method. - if ctx.Request().Method != http.MethodPost { - err := doErr(ctx, echo.NewHTTPError(http.StatusMethodNotAllowed)) - return ctx.LogRequestResult(err, false) - } - // validate Content-Type. - contentType := ctx.Request().Header.Get("Content-Type") - if !strings.Contains(contentType, "multipart/form-data") { - err := doErr(ctx, echo.NewHTTPError(http.StatusUnsupportedMediaType)) - return ctx.LogRequestResult(err, false) - } - // it's a multipart/form-data request, create a - // Resource. - if err := ctx.WithResource(trace); err != nil { - err = doCleanup(ctx, err) - err = doErr(ctx, err) - return ctx.LogRequestResult(err, false) - } - return next(ctx) - } - } -} - -// loggerMiddleware logs the result of a request. -func loggerMiddleware(config conf.Config) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - ctx := context.MustCastFromEchoContext(c) - err := next(ctx) - // we do not want to log healthcheck requests if - // log level is not set to DEBUG. - isDebug := ctx.Path() == pingEndpoint(config) - return ctx.LogRequestResult(err, isDebug) - } - } -} - -// cleanupMiddleware removes a resource.Resource -// at the end of a request. -func cleanupMiddleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - err := next(c) - ctx := context.MustCastFromEchoContext(c) - return doCleanup(ctx, err) - } - } -} - -// errorMiddleware handles errors (if any). -func errorMiddleware() echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - ctx := context.MustCastFromEchoContext(c) - err := next(ctx) - if err == nil { - // so far so good! - return nil - } - return doErr(ctx, err) - } - } -} - -func doCleanup(ctx context.Context, err error) error { - const op string = "xhttp.cleanup" - if !ctx.HasResource() { - // nothing to remove. - return err - } - r := ctx.MustResource() - // if a webhook URL has been given, - // do not remove the resource.Resource here because - // we don't know if the result file has been - // generated or sent. - if r.HasArg(resource.WebhookURLArgKey) { - return err - } - // a resource.Resource is associated with our custom context. - if resourceErr := r.Close(); resourceErr != nil { - xerr := xerror.New(op, resourceErr) - ctx.XLogger().ErrorOp(xerror.Op(xerr), xerr) - } - return err -} - -func doErr(ctx context.Context, err error) error { - // if it's an error from echo - // like 404 not found and so on. - if echoHTTPErr, ok := err.(*echo.HTTPError); ok { - // required to have a correct status code. - ctx.Error(echoHTTPErr) - return echoHTTPErr - } - // we log the initial error before returning - // the HTTP error. - errOp := xerror.Op(err) - logger := ctx.XLogger() - logger.ErrorOp(errOp, err) - // handle our custom HTTP error. - var httpErr error - errCode := xerror.Code(err) - errMessage := xerror.Message(err) - switch errCode { - case xerror.InvalidCode: - httpErr = echo.NewHTTPError(http.StatusBadRequest, errMessage) - case xerror.TimeoutCode: - httpErr = echo.NewHTTPError(http.StatusGatewayTimeout, errMessage) - default: - httpErr = echo.NewHTTPError(http.StatusInternalServerError, errMessage) - } - // required to have a correct status code. - ctx.Error(httpErr) - return httpErr -} diff --git a/internal/app/xhttp/option.go b/internal/app/xhttp/option.go deleted file mode 100644 index 335c7cd0..00000000 --- a/internal/app/xhttp/option.go +++ /dev/null @@ -1,114 +0,0 @@ -package xhttp - -import ( - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -func mergePrinterOptions(r resource.Resource, config conf.Config) (printer.MergePrinterOptions, error) { - const op string = "xhttp.mergePrinterOptions" - waitTimeout, err := resource.WaitTimeoutArg(r, config) - if err != nil { - return printer.MergePrinterOptions{}, xerror.New(op, err) - } - return printer.MergePrinterOptions{ - WaitTimeout: waitTimeout, - }, nil -} - -func chromePrinterOptions(r resource.Resource, config conf.Config) (printer.ChromePrinterOptions, error) { - const op string = "xhttp.chromePrinterOptions" - resolver := func() (printer.ChromePrinterOptions, error) { - waitTimeout, err := resource.WaitTimeoutArg(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - waitDelay, err := resource.WaitDelayArg(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - headerHTML, footerHTML, - err := resource.HeaderFooterContents(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - paperWidth, paperHeight, - err := resource.PaperSizeArgs(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - marginTop, marginBottom, marginLeft, marginRight, - err := resource.MarginArgs(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - landscape, err := r.BoolArg(resource.LandscapeArgKey, false) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - pageRanges, err := r.StringArg(resource.PageRangesArgKey, "") - if err != nil { - return printer.ChromePrinterOptions{}, err - } - googleChromeRpccBufferSize, err := resource.GoogleChromeRpccBufferSizeArg(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - scale, err := resource.ScaleArg(r, config) - if err != nil { - return printer.ChromePrinterOptions{}, err - } - return printer.ChromePrinterOptions{ - WaitTimeout: waitTimeout, - WaitDelay: waitDelay, - HeaderHTML: headerHTML, - FooterHTML: footerHTML, - PaperWidth: paperWidth, - PaperHeight: paperHeight, - MarginTop: marginTop, - MarginBottom: marginBottom, - MarginLeft: marginLeft, - MarginRight: marginRight, - Landscape: landscape, - PageRanges: pageRanges, - RpccBufferSize: googleChromeRpccBufferSize, - CustomHTTPHeaders: make(map[string]string), - Scale: scale, - }, nil - } - opts, err := resolver() - if err != nil { - return opts, xerror.New(op, err) - } - return opts, nil -} - -func officePrinterOptions(r resource.Resource, config conf.Config) (printer.OfficePrinterOptions, error) { - const op string = "xhttp.officePrinterOptions" - resolver := func() (printer.OfficePrinterOptions, error) { - waitTimeout, err := resource.WaitTimeoutArg(r, config) - if err != nil { - return printer.OfficePrinterOptions{}, err - } - landscape, err := r.BoolArg(resource.LandscapeArgKey, false) - if err != nil { - return printer.OfficePrinterOptions{}, err - } - pageRanges, err := r.StringArg(resource.PageRangesArgKey, "") - if err != nil { - return printer.OfficePrinterOptions{}, err - } - return printer.OfficePrinterOptions{ - WaitTimeout: waitTimeout, - Landscape: landscape, - PageRanges: pageRanges, - }, nil - } - opts, err := resolver() - if err != nil { - return opts, xerror.New(op, err) - } - return opts, nil -} diff --git a/internal/app/xhttp/pkg/context/context.go b/internal/app/xhttp/pkg/context/context.go deleted file mode 100644 index 908db2d7..00000000 --- a/internal/app/xhttp/pkg/context/context.go +++ /dev/null @@ -1,210 +0,0 @@ -package context - -import ( - "fmt" - "io" - "net/http" - "path/filepath" - "reflect" - "strconv" - "strings" - "time" - - "github.com/labstack/echo/v4" - "github.com/thecodingmachine/gotenberg/internal/app/xhttp/pkg/resource" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// Context extends the default echo.Context. -type Context struct { - echo.Context - logger xlog.Logger - config conf.Config - resource resource.Resource - startTime time.Time -} - -// New creates a new Context. -func New(c echo.Context, logger xlog.Logger, config conf.Config) Context { - return Context{ - c, - logger, - config, - resource.Resource{}, - time.Now(), - } -} - -/* -MustCastFromEchoContext cast an echo.Context -to our custom Context. - -It panics if casting goes wrong. -*/ -func MustCastFromEchoContext(c echo.Context) Context { - const op string = "context.MustCastFromEchoContext" - ctx, ok := c.(Context) - if !ok { - panic(fmt.Sprintf("%s: unable to cast an echo.Context to our custom context.Context", op)) - } - return ctx -} - -/* -XLogger returns the xlog.Logger associated -with the Context. - -This method should be used instead of the -default Logger() method coming from -the echo.Context. -*/ -func (ctx Context) XLogger() xlog.Logger { - return ctx.logger -} - -// Config returns the conf.Config associated -// with the Context. -func (ctx Context) Config() conf.Config { - return ctx.config -} - -// WithResource creates a resource.Resource and -// adds it to the Context. -func (ctx *Context) WithResource(directoryName string) error { - const op string = "context.Context.WithResource" - resolver := func() (resource.Resource, error) { - r, err := resource.New(ctx.logger, directoryName) - if err != nil { - return r, err - } - // retrieve custom headers from request. - for key, value := range ctx.Request().Header { - r.WithCustomHTTPHeader(key, value[0]) - } - // retrieve form values from request. - for _, key := range resource.ArgKeys() { - r.WithArg(key, ctx.FormValue(string(key))) - } - // write form files from request. - form, err := ctx.MultipartForm() - if err != nil { - /* - (very) special case: one and - only one file has been sent - and it is empty. - */ - if strings.Contains(err.Error(), io.EOF.Error()) { - return r, xerror.Invalid(op, "one file has been sent but it is empty: does it exist?", err) - } - return r, err - } - for _, files := range form.File { - for _, fh := range files { - in, err := fh.Open() - if err != nil { - return r, err - } - defer in.Close() - // avoid directory traversal. - filename := filepath.Base(fh.Filename) - if err := r.WithFile(filename, in); err != nil { - return r, err - } - } - } - return r, nil - } - resource, err := resolver() - ctx.resource = resource - if err != nil { - return xerror.New(op, err) - } - return nil -} - -// HasResource returns true if the Context -// has a resource.Resource. -func (ctx Context) HasResource() bool { - return !reflect.DeepEqual(ctx.resource, resource.Resource{}) -} - -/* -MustResource returns the resource.Resource -associated with the Context. - -It panics if no resource.Resource. -*/ -func (ctx Context) MustResource() resource.Resource { - const op string = "context.Context.MustResource" - if !ctx.HasResource() { - panic(fmt.Sprintf("%s: unable to retrieve the resource.Resource from our custom context.Context", op)) - } - return ctx.resource -} - -/* -LogRequestResult logs the result of a request. -This method should only be used by a middleware! - -If an error is given, returns the exact same error. -*/ -func (ctx Context) LogRequestResult(err error, isDebug bool) error { - const op string = "context.Context.LogRequestResult" - req := ctx.Request() - resp := ctx.Response() - stopTime := time.Now() - fields := map[string]interface{}{ - "remote_ip": ctx.RealIP(), - "host": req.Host, - "uri": req.RequestURI, - "method": req.Method, - "path": path(req), - "referer": req.Referer(), - "user_agent": req.UserAgent(), - "status": resp.Status, - "latency": lantency(ctx.startTime, stopTime), - "latency_human": latencyHuman(ctx.startTime, stopTime), - "bytes_in": bytesIn(req), - "bytes_out": bytesOut(resp), - } - if err != nil { - ctx.logger.WithFields(fields).ErrorOpf(op, "request failed") - return err - } - if isDebug { - ctx.logger.WithFields(fields).DebugOpf(op, "request handled") - return nil - } - ctx.logger.WithFields(fields).InfoOpf(op, "request handled") - return nil -} - -func path(r *http.Request) string { - path := r.URL.Path - if path == "" { - path = "/" - } - return path -} - -func lantency(startTime time.Time, stopTime time.Time) string { - return strconv.FormatInt(int64(stopTime.Sub(startTime)), 10) -} - -func latencyHuman(startTime time.Time, stopTime time.Time) string { - return stopTime.Sub(startTime).String() -} - -func bytesIn(r *http.Request) string { - bytesIn := r.Header.Get(echo.HeaderContentLength) - if bytesIn == "" { - bytesIn = "0" - } - return bytesIn -} - -func bytesOut(r *echo.Response) string { - return strconv.FormatInt(r.Size, 10) -} diff --git a/internal/app/xhttp/pkg/context/context_test.go b/internal/app/xhttp/pkg/context/context_test.go deleted file mode 100644 index 9b803460..00000000 --- a/internal/app/xhttp/pkg/context/context_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package context - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestMustCastFromEchoContext(t *testing.T) { - // should be OK. - ctx := New( - test.DummyEchoContext(), - test.DebugLogger(), - conf.DefaultConfig(), - ) - assert.NotPanics(t, func() { - result := MustCastFromEchoContext(ctx) - assert.Equal(t, ctx, result) - }) - // should not be OK. - assert.Panics(t, func() { - MustCastFromEchoContext(test.DummyEchoContext()) - }) -} - -func TestLogRequestResult(t *testing.T) { - ctx := New( - test.DummyEchoContext(), - test.DebugLogger(), - conf.DefaultConfig(), - ) - // Info log. - err := ctx.LogRequestResult(nil, false) - assert.Nil(t, err) - // Debug log. - err = ctx.LogRequestResult(nil, true) - assert.Nil(t, err) - // Error log. - err = ctx.LogRequestResult(errors.New("foo"), true) - assert.NotNil(t, err) -} - -func TestGetters(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - config := conf.DefaultConfig() - ctx := New( - test.DummyEchoContext(), - logger, - config, - ) - // Logger. - assert.Equal(t, logger, ctx.XLogger()) - // Config. - assert.Equal(t, config, ctx.Config()) - // Context should not have a resource.Resource. - assert.Equal(t, false, ctx.HasResource()) - assert.Panics(t, func() { - ctx.MustResource() - }) - // Context should have a resource.Resource. - ctx = New( - test.EchoContextMultipart(t), - logger, - config, - ) - err := ctx.WithResource(resourceDirectoryName) - assert.Nil(t, err) - assert.Equal(t, true, ctx.HasResource()) - assert.NotPanics(t, func() { - r := ctx.MustResource() - err = r.Close() - assert.Nil(t, err) - }) -} diff --git a/internal/app/xhttp/pkg/context/doc.go b/internal/app/xhttp/pkg/context/doc.go deleted file mode 100644 index 294bb860..00000000 --- a/internal/app/xhttp/pkg/context/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -/* -Package context extends the default echo.Context. - -All functions return our standard xerror.Error -in case of error. -*/ -package context diff --git a/internal/app/xhttp/pkg/resource/arg.go b/internal/app/xhttp/pkg/resource/arg.go deleted file mode 100644 index 2b4b70d1..00000000 --- a/internal/app/xhttp/pkg/resource/arg.go +++ /dev/null @@ -1,319 +0,0 @@ -package resource - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/internal/pkg/xassert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// ArgKey is a type for -// arguments' keys. -type ArgKey string - -const ( - // ResultFilenameArgKey is the key - // of the argument "resultFilename". - ResultFilenameArgKey ArgKey = "resultFilename" - // WaitTimeoutArgKey is the key - // of the argument "waitTimeout". - WaitTimeoutArgKey ArgKey = "waitTimeout" - // WebhookURLArgKey is the key - // of the argument "webhookURL". - WebhookURLArgKey ArgKey = "webhookURL" - // WebhookURLTimeoutArgKey is the key - // of the argument "webhookURLTimeout". - WebhookURLTimeoutArgKey ArgKey = "webhookURLTimeout" - // RemoteURLArgKey is the key - // of the argument "remoteURL". - RemoteURLArgKey ArgKey = "remoteURL" - // WaitDelayArgKey is the key - // of the argument "waitDelay". - WaitDelayArgKey ArgKey = "waitDelay" - // PaperWidthArgKey is the key - // of the argument "paperWidth". - PaperWidthArgKey ArgKey = "paperWidth" - // PaperHeightArgKey is the key - // of the argument "paperHeight". - PaperHeightArgKey ArgKey = "paperHeight" - // MarginTopArgKey is the key - // of the argument "marginTop". - MarginTopArgKey ArgKey = "marginTop" - // MarginBottomArgKey is the key - // of the argument "marginBottom". - MarginBottomArgKey ArgKey = "marginBottom" - // MarginLeftArgKey is the key - // of the argument "marginLeft". - MarginLeftArgKey ArgKey = "marginLeft" - // MarginRightArgKey is the key - // of the argument "marginRight". - MarginRightArgKey ArgKey = "marginRight" - // LandscapeArgKey is the key - // of the argument "landscape". - LandscapeArgKey ArgKey = "landscape" - // PageRangesArgKey is the key - // of the argument "pageRanges". - PageRangesArgKey ArgKey = "pageRanges" - // GoogleChromeRpccBufferSizeArgKey is the key - // of the argument "googleChromeRpccBufferSize". - GoogleChromeRpccBufferSizeArgKey ArgKey = "googleChromeRpccBufferSize" - // ScaleArgKey is the key - // of the argument "scale". - ScaleArgKey ArgKey = "scale" -) - -/* -ArgKeys returns a slice -containing all available -arguments' keys. -*/ -func ArgKeys() []ArgKey { - return []ArgKey{ - ResultFilenameArgKey, - WaitTimeoutArgKey, - WebhookURLArgKey, - WebhookURLTimeoutArgKey, - RemoteURLArgKey, - WaitDelayArgKey, - PaperWidthArgKey, - PaperHeightArgKey, - MarginTopArgKey, - MarginBottomArgKey, - MarginLeftArgKey, - MarginRightArgKey, - LandscapeArgKey, - PageRangesArgKey, - GoogleChromeRpccBufferSizeArgKey, - ScaleArgKey, - } -} - -/* -WaitTimeoutArg is a helper for retrieving -the "waitTimeout" argument as float64. - -It also validates it against the application -configuration. -*/ -func WaitTimeoutArg(r Resource, config conf.Config) (float64, error) { - const op string = "resource.WaitTimeoutArg" - result, err := r.Float64Arg( - WaitTimeoutArgKey, - config.DefaultWaitTimeout(), - xassert.Float64NotInferiorTo(0), - xassert.Float64NotSuperiorTo(config.MaximumWaitTimeout()), - ) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -WaitDelayArg is a helper for retrieving -the "waitDelay" argument as float64. - -It also validates it against the application -configuration. -*/ -func WaitDelayArg(r Resource, config conf.Config) (float64, error) { - const ( - op string = "resource.WaitDelayArg" - defaultWaitDelay float64 = 0.0 - ) - result, err := r.Float64Arg( - WaitDelayArgKey, - defaultWaitDelay, - xassert.Float64NotInferiorTo(0.0), - xassert.Float64NotSuperiorTo(config.MaximumWaitDelay()), - ) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -WebhookURLTimeoutArg is a helper for retrieving -the "webhookURLTimeout" argument as float64. - -It also validates it against the application -configuration. -*/ -func WebhookURLTimeoutArg(r Resource, config conf.Config) (float64, error) { - const op string = "resource.WebhookURLTimeoutArg" - result, err := r.Float64Arg( - WebhookURLTimeoutArgKey, - config.DefaultWebhookURLTimeout(), - xassert.Float64NotInferiorTo(0), - xassert.Float64NotSuperiorTo(config.MaximumWebhookURLTimeout()), - ) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -PaperSizeArgs is a helper for retrieving -the "paperWidth" and "paperHeight" arguments -as float64. -*/ -func PaperSizeArgs(r Resource, config conf.Config) (float64, float64, error) { - const op string = "resource.PaperSizeArgs" - opts := printer.DefaultChromePrinterOptions(config) - resolver := func() (float64, float64, error) { - paperWidth, err := r.Float64Arg( - PaperWidthArgKey, - opts.PaperWidth, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.PaperWidth, - opts.PaperHeight, - err - } - paperHeight, err := r.Float64Arg( - PaperHeightArgKey, - opts.PaperHeight, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.PaperWidth, - opts.PaperHeight, - err - } - return paperWidth, - paperHeight, - nil - } - paperWidth, paperHeight, - err := resolver() - if err != nil { - return paperWidth, - paperHeight, - xerror.New(op, err) - } - return paperWidth, - paperHeight, - nil -} - -/* -MarginArgs is a helper for retrieving -the "marginTop", "marginBottom", "marginLeft" -and "marginRight" arguments as float64. -*/ -func MarginArgs(r Resource, config conf.Config) (float64, float64, float64, float64, error) { - const op string = "resource.MarginArgs" - opts := printer.DefaultChromePrinterOptions(config) - resolver := func() (float64, float64, float64, float64, error) { - marginTop, err := r.Float64Arg( - MarginTopArgKey, - opts.MarginTop, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.MarginTop, - opts.MarginBottom, - opts.MarginLeft, - opts.MarginRight, - err - } - marginBottom, err := r.Float64Arg( - MarginBottomArgKey, - opts.MarginBottom, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.MarginTop, - opts.MarginBottom, - opts.MarginLeft, - opts.MarginRight, - err - } - marginLeft, err := r.Float64Arg( - MarginLeftArgKey, - opts.MarginLeft, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.MarginTop, - opts.MarginBottom, - opts.MarginLeft, - opts.MarginRight, - err - } - marginRight, err := r.Float64Arg( - MarginRightArgKey, - opts.MarginRight, - xassert.Float64NotInferiorTo(0.0), - ) - if err != nil { - return opts.MarginTop, - opts.MarginBottom, - opts.MarginLeft, - opts.MarginRight, - err - } - return marginTop, - marginBottom, - marginLeft, - marginRight, - nil - } - marginTop, marginBottom, marginLeft, marginRight, - err := resolver() - if err != nil { - return marginTop, - marginBottom, - marginLeft, - marginRight, - xerror.New(op, err) - } - return marginTop, - marginBottom, - marginLeft, - marginRight, - nil -} - -/* -GoogleChromeRpccBufferSizeArg is a helper for retrieving -the "googleChromeRpccBufferSize" argument as int64. - -It also validates it against the application -configuration. -*/ -func GoogleChromeRpccBufferSizeArg(r Resource, config conf.Config) (int64, error) { - const op string = "resource.GoogleChromeRpccBufferSizeArg" - result, err := r.Int64Arg( - GoogleChromeRpccBufferSizeArgKey, - config.DefaultGoogleChromeRpccBufferSize(), - xassert.Int64NotInferiorTo(0.0), - xassert.Int64NotSuperiorTo(config.MaximumGoogleChromeRpccBufferSize()), - ) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -ScaleArg is a helper for retrieving -the "scale" argument as float64. -*/ -func ScaleArg(r Resource, config conf.Config) (float64, error) { - const op string = "resource.ScaleArg" - opts := printer.DefaultChromePrinterOptions(config) - result, err := r.Float64Arg( - ScaleArgKey, - opts.Scale, - xassert.Float64NotInferiorTo(0.1), - xassert.Float64NotSuperiorTo(2.0), - ) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} diff --git a/internal/app/xhttp/pkg/resource/arg_test.go b/internal/app/xhttp/pkg/resource/arg_test.go deleted file mode 100644 index 0fefba18..00000000 --- a/internal/app/xhttp/pkg/resource/arg_test.go +++ /dev/null @@ -1,396 +0,0 @@ -package resource - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestArgKeys(t *testing.T) { - expected := []ArgKey{ - ResultFilenameArgKey, - WaitTimeoutArgKey, - WebhookURLArgKey, - WebhookURLTimeoutArgKey, - RemoteURLArgKey, - WaitDelayArgKey, - PaperWidthArgKey, - PaperHeightArgKey, - MarginTopArgKey, - MarginBottomArgKey, - MarginLeftArgKey, - MarginRightArgKey, - LandscapeArgKey, - PageRangesArgKey, - GoogleChromeRpccBufferSizeArgKey, - ScaleArgKey, - } - assert.Equal(t, expected, ArgKeys()) -} - -func TestWaitTimeoutArg(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // argument does not exist. - expected = config.DefaultWaitTimeout() - v, err := WaitTimeoutArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // argument exist. - expected = 5.0 - r.WithArg(WaitTimeoutArgKey, "5.0") - v, err = WaitTimeoutArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is < 0. - expected = config.DefaultWaitTimeout() - r.WithArg(WaitTimeoutArgKey, "-1.0") - v, err = WaitTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is > config.MaximumWaitTimeout(). - expected = config.DefaultWaitTimeout() - r.WithArg(WaitTimeoutArgKey, "31.0") - v, err = WaitTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as - // argument value is invalid. - expected = config.DefaultWaitTimeout() - r.WithArg(WaitTimeoutArgKey, "foo") - v, err = WaitTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestWaitDelayArg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue float64 = 0.0 - ) - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // argument does not exist. - expected = defaultValue - v, err := WaitDelayArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // argument exist. - expected = 5.0 - r.WithArg(WaitDelayArgKey, "5.0") - v, err = WaitDelayArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is < 0. - expected = defaultValue - r.WithArg(WaitDelayArgKey, "-1.0") - v, err = WaitDelayArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is > config.MaximumWaitDelay(). - expected = defaultValue - r.WithArg(WaitDelayArgKey, "31.0") - v, err = WaitDelayArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as - // argument value is invalid. - expected = defaultValue - r.WithArg(WaitDelayArgKey, "foo") - v, err = WaitDelayArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestWebhookURLTimeoutArg(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // argument does not exist. - expected = config.DefaultWebhookURLTimeout() - v, err := WebhookURLTimeoutArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // argument exist. - expected = 5.0 - r.WithArg(WebhookURLTimeoutArgKey, "5.0") - v, err = WebhookURLTimeoutArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is < 0. - expected = config.DefaultWebhookURLTimeout() - r.WithArg(WebhookURLTimeoutArgKey, "-1.0") - v, err = WebhookURLTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is > config.MaximumWebhookURLTimeout(). - expected = config.DefaultWebhookURLTimeout() - r.WithArg(WebhookURLTimeoutArgKey, "31.0") - v, err = WebhookURLTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as - // argument value is invalid. - expected = config.DefaultWebhookURLTimeout() - r.WithArg(WebhookURLTimeoutArgKey, "foo") - v, err = WebhookURLTimeoutArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestPaperSizeArgs(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - opts := printer.DefaultChromePrinterOptions(config) - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // arguments do not exist. - width, height, err := PaperSizeArgs(r, config) - assert.Nil(t, err) - assert.Equal(t, opts.PaperWidth, width) - assert.Equal(t, opts.PaperHeight, height) - // arguments exist. - expected = 5.0 - r.WithArg(PaperWidthArgKey, "5.0") - r.WithArg(PaperHeightArgKey, "5.0") - width, height, err = PaperSizeArgs(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, width) - assert.Equal(t, expected, height) - // should not be OK as arguments - // value are < 0. - expected = opts.PaperWidth - r.WithArg(PaperWidthArgKey, "-1.0") - width, _, err = PaperSizeArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, width) - r.WithArg(PaperWidthArgKey, "5.0") - expected = opts.PaperHeight - r.WithArg(PaperHeightArgKey, "-1.0") - _, height, err = PaperSizeArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, height) - r.WithArg(PaperHeightArgKey, "5.0") - // should not be OK as - // arguments value are invalids. - expected = opts.PaperWidth - r.WithArg(PaperWidthArgKey, "foo") - width, _, err = PaperSizeArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, width) - r.WithArg(PaperWidthArgKey, "5.0") - expected = opts.PaperHeight - r.WithArg(PaperHeightArgKey, "foo") - _, height, err = PaperSizeArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, height) - r.WithArg(PaperHeightArgKey, "5.0") - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestMarginArgs(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - opts := printer.DefaultChromePrinterOptions(config) - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // arguments do not exist. - top, bottom, left, right, err := MarginArgs(r, config) - assert.Nil(t, err) - assert.Equal(t, opts.MarginTop, top) - assert.Equal(t, opts.MarginBottom, bottom) - assert.Equal(t, opts.MarginLeft, left) - assert.Equal(t, opts.MarginRight, right) - // arguments exist. - expected = 5.0 - r.WithArg(MarginTopArgKey, "5.0") - r.WithArg(MarginBottomArgKey, "5.0") - r.WithArg(MarginLeftArgKey, "5.0") - r.WithArg(MarginRightArgKey, "5.0") - top, bottom, left, right, err = MarginArgs(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, top) - assert.Equal(t, expected, bottom) - assert.Equal(t, expected, left) - assert.Equal(t, expected, right) - // should not be OK as arguments - // value are < 0. - expected = opts.MarginTop - r.WithArg(MarginTopArgKey, "-1.0") - top, _, _, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, top) - r.WithArg(MarginTopArgKey, "5.0") - expected = opts.MarginBottom - r.WithArg(MarginBottomArgKey, "-1.0") - _, bottom, _, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, bottom) - r.WithArg(MarginBottomArgKey, "5.0") - expected = opts.MarginLeft - r.WithArg(MarginLeftArgKey, "-1.0") - _, _, left, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, left) - r.WithArg(MarginLeftArgKey, "5.0") - expected = opts.MarginRight - r.WithArg(MarginRightArgKey, "-1.0") - _, _, _, right, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, right) - r.WithArg(MarginRightArgKey, "5.0") - // should not be OK as - // arguments value are invalids. - expected = opts.MarginTop - r.WithArg(MarginTopArgKey, "foo") - top, _, _, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, top) - r.WithArg(MarginTopArgKey, "5.0") - expected = opts.MarginBottom - r.WithArg(MarginBottomArgKey, "foo") - _, bottom, _, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, bottom) - r.WithArg(MarginBottomArgKey, "5.0") - expected = opts.MarginLeft - r.WithArg(MarginLeftArgKey, "foo") - _, _, left, _, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, left) - r.WithArg(MarginLeftArgKey, "5.0") - expected = opts.MarginRight - r.WithArg(MarginRightArgKey, "foo") - _, _, _, right, err = MarginArgs(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, right) - r.WithArg(MarginRightArgKey, "5.0") - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestGoogleChromeRpccBufferSizeArg(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected int64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // argument does not exist. - expected = config.DefaultGoogleChromeRpccBufferSize() - v, err := GoogleChromeRpccBufferSizeArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // argument exist. - expected = 10 - r.WithArg(GoogleChromeRpccBufferSizeArgKey, "10") - v, err = GoogleChromeRpccBufferSizeArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is < 0. - expected = config.DefaultGoogleChromeRpccBufferSize() - r.WithArg(GoogleChromeRpccBufferSizeArgKey, "-1") - v, err = GoogleChromeRpccBufferSizeArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is > config.MaximumGoogleChromeRpccBufferSize(). - expected = config.DefaultGoogleChromeRpccBufferSize() - r.WithArg(GoogleChromeRpccBufferSizeArgKey, "104857601") - v, err = GoogleChromeRpccBufferSizeArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as - // argument value is invalid. - expected = config.DefaultGoogleChromeRpccBufferSize() - r.WithArg(GoogleChromeRpccBufferSizeArgKey, "foo") - v, err = GoogleChromeRpccBufferSizeArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestScaleArg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue float64 = 1.0 - ) - var expected float64 - logger := test.DebugLogger() - config := conf.DefaultConfig() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // argument does not exist. - expected = defaultValue - v, err := ScaleArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // argument exist. - expected = 0.5 - r.WithArg(ScaleArgKey, "0.5") - v, err = ScaleArg(r, config) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is < 0.1. - expected = defaultValue - r.WithArg(ScaleArgKey, "-1.0") - v, err = ScaleArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as argument - // value is > 2.0. - expected = defaultValue - r.WithArg(ScaleArgKey, "2.1") - v, err = ScaleArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // should not be OK as - // argument value is invalid. - expected = defaultValue - r.WithArg(ScaleArgKey, "foo") - v, err = ScaleArg(r, config) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} diff --git a/internal/app/xhttp/pkg/resource/doc.go b/internal/app/xhttp/pkg/resource/doc.go deleted file mode 100644 index 3ec7b2c9..00000000 --- a/internal/app/xhttp/pkg/resource/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package resource helps managing -arguments and files for a conversion. - -All functions return our standard xerror.Error -in case of error. -*/ -package resource diff --git a/internal/app/xhttp/pkg/resource/file.go b/internal/app/xhttp/pkg/resource/file.go deleted file mode 100644 index 5526cf90..00000000 --- a/internal/app/xhttp/pkg/resource/file.go +++ /dev/null @@ -1,91 +0,0 @@ -package resource - -import ( - "io" - "io/ioutil" - "os" - - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// file represents a file within the resource. -type file struct { - fpath string -} - -// write writes given content to the -// resourceFile location. -func (f file) write(in io.Reader) error { - const op string = "resource.file.write" - resolver := func() error { - out, err := os.Create(f.fpath) - if err != nil { - return err - } - defer out.Close() - if err := out.Chmod(0600); err != nil { - return err - } - if _, err := io.Copy(out, in); err != nil { - return err - } - if _, err := out.Seek(0, 0); err != nil { - return err - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// content returns the string content of -// the file. -func (f file) content() (string, error) { - const op string = "resource.file.content" - b, err := ioutil.ReadFile(f.fpath) - if err != nil { - return "", xerror.New(op, err) - } - return string(b), nil -} - -/* -HeaderFooterContents is a helper for retrieving -the content of the files "header.html" -and "footer.html". -*/ -func HeaderFooterContents(r Resource, config conf.Config) (string, string, error) { - const op string = "resource.HeaderFooterContents" - opts := printer.DefaultChromePrinterOptions(config) - resolver := func() (string, string, error) { - headerHTML, err := r.Fcontent("header.html", opts.HeaderHTML) - if err != nil { - return opts.HeaderHTML, - opts.FooterHTML, - err - } - footerHTML, err := r.Fcontent("footer.html", opts.FooterHTML) - if err != nil { - return opts.HeaderHTML, - opts.FooterHTML, - err - } - return headerHTML, - footerHTML, - nil - } - headerHTML, footerHTML, - err := resolver() - if err != nil { - return headerHTML, - footerHTML, - xerror.New(op, err) - } - return headerHTML, - footerHTML, - nil -} diff --git a/internal/app/xhttp/pkg/resource/file_test.go b/internal/app/xhttp/pkg/resource/file_test.go deleted file mode 100644 index a376f8f2..00000000 --- a/internal/app/xhttp/pkg/resource/file_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package resource - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/printer" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestHeaderFooterContents(t *testing.T) { - const resourceDirectoryName string = "foo" - var expected string - logger := test.DebugLogger() - config := conf.DefaultConfig() - opts := printer.DefaultChromePrinterOptions(config) - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // arguments do not exist. - header, footer, err := HeaderFooterContents(r, config) - assert.Nil(t, err) - assert.Equal(t, opts.HeaderHTML, header) - assert.Equal(t, opts.FooterHTML, footer) - // arguments exist. - expected = "Gutenberg" - fpath := test.OfficeFpaths(t)[2] - f1, err := os.Open(fpath) - assert.Nil(t, err) - defer f1.Close() // nolint: errcheck - err = r.WithFile("header.html", f1) - assert.Nil(t, err) - f2, err := os.Open(fpath) - assert.Nil(t, err) - defer f2.Close() // nolint: errcheck - err = r.WithFile("footer.html", f2) - assert.Nil(t, err) - header, footer, err = HeaderFooterContents(r, config) - assert.Nil(t, err) - assert.Contains(t, header, expected) - assert.Contains(t, footer, expected) - // finally... - err = r.Close() - assert.Nil(t, err) -} diff --git a/internal/app/xhttp/pkg/resource/header.go b/internal/app/xhttp/pkg/resource/header.go deleted file mode 100644 index fa4c8814..00000000 --- a/internal/app/xhttp/pkg/resource/header.go +++ /dev/null @@ -1,37 +0,0 @@ -package resource - -import ( - "strings" -) - -const ( - // RemoteURLCustomHTTPHeaderCanonicalBaseKey is the base key - // of custom headers send to the remote URL. - RemoteURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Remoteurl-" - // WebhookURLCustomHTTPHeaderCanonicalBaseKey is the base key - // of custom headers send to the webhook URL. - WebhookURLCustomHTTPHeaderCanonicalBaseKey string = "Gotenberg-Webhookurl-" -) - -func fetchCustomHTTPHeaders(r Resource, baseKey string) map[string]string { - customHeaders := make(map[string]string) - for key, value := range r.customHeaders { - if strings.Contains(key, baseKey) { - realKey := strings.Replace(key, baseKey, "", 1) - customHeaders[realKey] = value - } - } - return customHeaders -} - -// RemoteURLCustomHTTPHeaders is a helper for retrieving -// the custom headers for the URL conversion. -func RemoteURLCustomHTTPHeaders(r Resource) map[string]string { - return fetchCustomHTTPHeaders(r, RemoteURLCustomHTTPHeaderCanonicalBaseKey) -} - -// WebhookURLCustomHTTPHeaders is a helper for retrieving -// the custom headers for the webhook URL. -func WebhookURLCustomHTTPHeaders(r Resource) map[string]string { - return fetchCustomHTTPHeaders(r, WebhookURLCustomHTTPHeaderCanonicalBaseKey) -} diff --git a/internal/app/xhttp/pkg/resource/header_test.go b/internal/app/xhttp/pkg/resource/header_test.go deleted file mode 100644 index 35a944f8..00000000 --- a/internal/app/xhttp/pkg/resource/header_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package resource - -import ( - "fmt" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestRemoteURLCustomHeaders(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // should find the custom header. - customHeaderValue := "bar" - customHeaderCanonicalRealKey := "Foo" - customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", RemoteURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) - r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue) - r.WithCustomHTTPHeader("Bar", "Bar") - expected := map[string]string{ - customHeaderCanonicalRealKey: customHeaderValue, - } - notExpected := map[string]string{ - customHeaderCanonicalKey: customHeaderValue, - } - v := RemoteURLCustomHTTPHeaders(r) - assert.Equal(t, expected, v) - assert.NotEqual(t, notExpected, v) -} - -func TestWebhookURLCustomHeaders(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // should find the custom header. - customHeaderValue := "bar" - customHeaderCanonicalRealKey := "Foo" - customHeaderCanonicalKey := http.CanonicalHeaderKey(fmt.Sprintf("%s%s", WebhookURLCustomHTTPHeaderCanonicalBaseKey, customHeaderCanonicalRealKey)) - r.WithCustomHTTPHeader(customHeaderCanonicalKey, customHeaderValue) - r.WithCustomHTTPHeader("Bar", "Bar") - expected := map[string]string{ - customHeaderCanonicalRealKey: customHeaderValue, - } - notExpected := map[string]string{ - customHeaderCanonicalKey: customHeaderValue, - } - v := WebhookURLCustomHTTPHeaders(r) - assert.Equal(t, expected, v) - assert.NotEqual(t, notExpected, v) -} diff --git a/internal/app/xhttp/pkg/resource/resource.go b/internal/app/xhttp/pkg/resource/resource.go deleted file mode 100644 index 44b64599..00000000 --- a/internal/app/xhttp/pkg/resource/resource.go +++ /dev/null @@ -1,258 +0,0 @@ -package resource - -import ( - "fmt" - "io" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/thecodingmachine/gotenberg/internal/pkg/normalize" - "github.com/thecodingmachine/gotenberg/internal/pkg/xassert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -/* -TemporaryDirectory is the directory -where all the resources directory -are located. -*/ -const TemporaryDirectory string = "tmp" - -// Resource helps managing -// arguments and files for a conversion. -type Resource struct { - logger xlog.Logger - dirPath string - customHeaders map[string]string - args map[ArgKey]string - files map[string]file -} - -// New creates a Resource where its files will -// be located in the given directory name. -func New(logger xlog.Logger, directoryName string) (Resource, error) { - const op string = "resource.New" - resolver := func() (string, error) { - dirPath := fmt.Sprintf("%s/%s", TemporaryDirectory, directoryName) - if err := os.MkdirAll(dirPath, 0755); err != nil { - return "", err - } - absDirPath, err := filepath.Abs(dirPath) - if err != nil { - return "", err - } - return absDirPath, nil - } - dirPath, err := resolver() - if err != nil { - return Resource{}, xerror.New(op, err) - } - logger.DebugOpf(op, "resource directory '%s' created", directoryName) - return Resource{ - logger: logger, - dirPath: dirPath, - customHeaders: make(map[string]string), - args: make(map[ArgKey]string), - files: make(map[string]file), - }, nil -} - -// Close removes the working directory of the -// Resource if it exists. -func (r Resource) Close() error { - const op string = "resource.Resource.Close" - if _, err := os.Stat(r.dirPath); os.IsNotExist(err) { - r.logger.DebugOpf(op, "resource directory '%s' does not exist, nothing to remove", r.dirPath) - return nil - } - if err := os.RemoveAll(r.dirPath); err != nil { - return xerror.New(op, err) - } - r.logger.DebugOpf(op, "resource directory '%s' removed", r.dirPath) - return nil -} - -// WithCustomHTTPHeader add a new custom header to the Resource. -// Given key should be in canonical format. -func (r *Resource) WithCustomHTTPHeader(key string, value string) { - const op string = "resource.Resource.WithCustomHTTPHeader" - // should already be in canonical format. - canonicalKey := http.CanonicalHeaderKey(key) - if strings.Contains(canonicalKey, RemoteURLCustomHTTPHeaderCanonicalBaseKey) || - strings.Contains(canonicalKey, WebhookURLCustomHTTPHeaderCanonicalBaseKey) { - r.customHeaders[canonicalKey] = value - r.logger.DebugOpf(op, "added '%s' with value '%s' to resource custom HTTP headers", canonicalKey, value) - return - } - r.logger.DebugOpf(op, "skipping '%s' as it is not a custom HTTP header...", canonicalKey) -} - -// WithArg add a new argument to the Resource. -func (r *Resource) WithArg(key ArgKey, value string) { - const op string = "resource.Resource.WithArg" - r.args[key] = value - r.logger.DebugOpf(op, "added '%s' with value '%s' to resource args", key, value) -} - -// WithFile add a new file to the Resource. -func (r *Resource) WithFile(filename string, in io.Reader) error { - const op string = "resource.Resource.WithFile" - resolver := func() error { - // see https://github.com/thecodingmachine/gotenberg/issues/104. - normalized, err := normalize.String(filename) - if err != nil { - return err - } - fpath := fmt.Sprintf("%s/%s", r.dirPath, normalized) - file := file{fpath: fpath} - if err := file.write(in); err != nil { - return err - } - r.files[filename] = file - r.logger.DebugOpf(op, "resource file '%s' created", filename) - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// DirPath returns the directory path -// of the Resource. -func (r Resource) DirPath() string { - return r.dirPath -} - -// HasArg returns true if given key exists -// among the Resource and its value is not empty. -func (r Resource) HasArg(key ArgKey) bool { - if v, ok := r.args[key]; ok { - return v != "" - } - return false -} - -/* -StringArg returns the value of the -argument identified by given key. - -It works in the same manner as xassert.String. -*/ -func (r Resource) StringArg(key ArgKey, defaultValue string, rules ...xassert.RuleString) (string, error) { - const op string = "resource.Resource.StringArg" - result, err := xassert.String(string(key), r.args[key], defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Int64Arg returns the int64 representation of the -argument identified by given key. - -It works in the same manner as xassert.Int64. -*/ -func (r Resource) Int64Arg(key ArgKey, defaultValue int64, rules ...xassert.RuleInt64) (int64, error) { - const op string = "resource.Resource.Int64Arg" - result, err := xassert.Int64(string(key), r.args[key], defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Float64Arg returns the float64 representation of the -argument identified by given key. - -It works in the same manner as xassert.Float64. -*/ -func (r Resource) Float64Arg(key ArgKey, defaultValue float64, rules ...xassert.RuleFloat64) (float64, error) { - const op string = "resource.Resource.Float64Arg" - result, err := xassert.Float64(string(key), r.args[key], defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -BoolArg returns the boolean representation of the -argument identified by given key. - -It works in the same manner as xassert.Bool. -*/ -func (r Resource) BoolArg(key ArgKey, defaultValue bool) (bool, error) { - const op string = "resource.Resource.BoolArg" - result, err := xassert.Bool(string(key), r.args[key], defaultValue) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -// Fpath returns the path of the given filename. -// This filename should exist whithin the Resource. -func (r Resource) Fpath(filename string) (string, error) { - const op string = "resource.Resource.Fpath" - file, ok := r.files[filename] - if !ok { - return "", xerror.Invalid( - op, - fmt.Sprintf("resource file '%s' does not exist", filename), - nil, - ) - } - return file.fpath, nil -} - -/* -Fpaths returns the paths of the files -having one of the given file extensions. - -It should found at least one path. -*/ -func (r Resource) Fpaths(exts ...string) ([]string, error) { - const op string = "resource.Resource.Fpaths" - var fpaths []string - for filename, file := range r.files { - for _, ext := range exts { - if filepath.Ext(filename) == ext { - fpaths = append(fpaths, file.fpath) - } - } - } - if len(fpaths) == 0 { - return nil, xerror.Invalid( - op, - fmt.Sprintf("no resource file found for extensions '%v'", exts), - nil, - ) - } - return fpaths, nil -} - -/* -Fcontent returns the string content of the -given filename. - -If filename does not exist within the Resource, -returns the default value. -*/ -func (r Resource) Fcontent(filename, defaultValue string) (string, error) { - const op string = "resource.Resource.Fcontent" - file, ok := r.files[filename] - if !ok { - return defaultValue, nil - } - content, err := file.content() - if err != nil { - return "", xerror.New(op, err) - } - return content, nil -} diff --git a/internal/app/xhttp/pkg/resource/resource_test.go b/internal/app/xhttp/pkg/resource/resource_test.go deleted file mode 100644 index 14b51dcd..00000000 --- a/internal/app/xhttp/pkg/resource/resource_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package resource - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xassert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestStringArg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue string = "FOO" - ) - var expected string - rule := xassert.StringOneOf([]string{"FOO", "BAR"}) - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // empty value, result should be equal - // to the default value. - r.WithArg(ResultFilenameArgKey, "") - v, err := r.StringArg(ResultFilenameArgKey, defaultValue) - assert.Nil(t, err) - assert.Equal(t, defaultValue, v) - // result should be equal to given value - // as it is one of "FOO" and "BAR". - expected = "BAR" - r.WithArg(ResultFilenameArgKey, expected) - v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as given value is not - // one of "FOO" and "BAR". - expected = defaultValue - r.WithArg(ResultFilenameArgKey, "BAZ") - v, err = r.StringArg(ResultFilenameArgKey, defaultValue, rule) - test.AssertError(t, err) - assert.Equal(t, expected, v) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestInt64Arg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue int64 = 10 - ) - var expected int64 - rule := xassert.Int64NotInferiorTo(6) - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // empty value, result should be equal - // to the default value. - r.WithArg(WaitTimeoutArgKey, "") - v, err := r.Int64Arg(WaitTimeoutArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as integer. - r.WithArg(WaitTimeoutArgKey, "5") - v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue) - expected = 5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of an integer. - r.WithArg(WaitTimeoutArgKey, "foo") - v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // should not be OK as given value does not - // validate the rule x >= 6. - r.WithArg(WaitTimeoutArgKey, "5") - v, err = r.Int64Arg(WaitTimeoutArgKey, defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestFloat64Arg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue float64 = 10.0 - ) - var expected float64 - rule := xassert.Float64NotInferiorTo(6.0) - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // empty value, result should be equal - // to the default value. - r.WithArg(WaitTimeoutArgKey, "") - v, err := r.Float64Arg(WaitTimeoutArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as float. - r.WithArg(WaitTimeoutArgKey, "5.5") - v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue) - expected = 5.5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of a float. - r.WithArg(WaitTimeoutArgKey, "foo") - v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // should not be OK as given value does not - // validate the rule x >= 6. - r.WithArg(WaitTimeoutArgKey, "5.0") - v, err = r.Float64Arg(WaitTimeoutArgKey, defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestBoolArg(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue bool = true - ) - var expected bool - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // empty value, result should be equal - // to the default value. - r.WithArg(LandscapeArgKey, "") - v, err := r.BoolArg(LandscapeArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as boolean. - r.WithArg(LandscapeArgKey, "1") - v, err = r.BoolArg(LandscapeArgKey, defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - r.WithArg(LandscapeArgKey, "true") - v, err = r.BoolArg(LandscapeArgKey, defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - r.WithArg(LandscapeArgKey, "0") - v, err = r.BoolArg(LandscapeArgKey, defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - r.WithArg(LandscapeArgKey, "false") - v, err = r.BoolArg(LandscapeArgKey, defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of a boolean. - r.WithArg(LandscapeArgKey, "foo") - v, err = r.BoolArg(LandscapeArgKey, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestFpath(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // file exists. - fpath := test.MergeFpaths(t)[0] - f, err := os.Open(fpath) - assert.Nil(t, err) - filename := "foo.pdf" - err = r.WithFile(filename, f) - assert.Nil(t, err) - absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName)) - assert.Nil(t, err) - expected := fmt.Sprintf("%s/%s", absDirPath, filename) - v, err := r.Fpath(filename) - assert.Nil(t, err) - assert.Equal(t, expected, v) - // should not be OK as file does - // not exist. - _, err = r.Fpath("bar.pdf") - test.AssertError(t, err) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestFpaths(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - fpath := test.MergeFpaths(t)[0] - f, err := os.Open(fpath) - assert.Nil(t, err) - defer f.Close() // nolint: errcheck - filename := "foo.pdf" - err = r.WithFile(filename, f) - assert.Nil(t, err) - // file extension exists. - absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName)) - assert.Nil(t, err) - expected := []string{ - fmt.Sprintf("%s/%s", absDirPath, filename), - } - fpaths, err := r.Fpaths(".pdf") - assert.Nil(t, err) - assert.Equal(t, expected, fpaths) - // should not be OK as file extension - // does not exist. - _, err = r.Fpaths(".html") - test.AssertError(t, err) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestFcontent(t *testing.T) { - const ( - resourceDirectoryName string = "foo" - defaultValue string = "Gutenberg" - ) - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - filename := "foo.txt" - // file does not exist, expecting - // value. - v, err := r.Fcontent(filename, defaultValue) - assert.Nil(t, err) - assert.Equal(t, defaultValue, v) - // file exists. - fpath := test.OfficeFpaths(t)[2] - f, err := os.Open(fpath) - assert.Nil(t, err) - defer f.Close() // nolint: errcheck - err = r.WithFile(filename, f) - assert.Nil(t, err) - v, err = r.Fcontent(filename, defaultValue) - assert.Nil(t, err) - assert.Contains(t, v, defaultValue) - // finally... - err = r.Close() - assert.Nil(t, err) -} - -func TestGetters(t *testing.T) { - const resourceDirectoryName string = "foo" - logger := test.DebugLogger() - r, err := New(logger, resourceDirectoryName) - assert.Nil(t, err) - // has arg. - assert.Equal(t, false, r.HasArg(LandscapeArgKey)) - r.WithArg(LandscapeArgKey, "foo") - assert.Equal(t, true, r.HasArg(LandscapeArgKey)) - // directory path. - absDirPath, err := filepath.Abs(fmt.Sprintf("%s/%s", TemporaryDirectory, resourceDirectoryName)) - assert.Nil(t, err) - assert.Equal(t, absDirPath, r.DirPath()) - // finally... - err = r.Close() - assert.Nil(t, err) -} diff --git a/internal/app/xhttp/xhttp.go b/internal/app/xhttp/xhttp.go deleted file mode 100644 index 02db10e1..00000000 --- a/internal/app/xhttp/xhttp.go +++ /dev/null @@ -1,31 +0,0 @@ -package xhttp - -import ( - "github.com/labstack/echo/v4" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" -) - -// New returns a custom echo.Echo. -func New(config conf.Config) *echo.Echo { - srv := echo.New() - srv.HideBanner = true - srv.HidePort = true - srv.Use(contextMiddleware(config)) - srv.Use(loggerMiddleware(config)) - srv.Use(cleanupMiddleware()) - srv.Use(errorMiddleware()) - srv.GET(pingEndpoint(config), pingHandler) - srv.POST(mergeEndpoint(config), mergeHandler) - if config.DisableGoogleChrome() && config.DisableUnoconv() { - return srv - } - if !config.DisableGoogleChrome() { - srv.POST(htmlEndpoint(config), htmlHandler) - srv.POST(urlEndpoint(config), urlHandler) - srv.POST(markdownEndpoint(config), markdownHandler) - } - if !config.DisableUnoconv() { - srv.POST(officeEndpoint(config), officeHandler) - } - return srv -} diff --git a/internal/app/xhttp/xhttp_test.go b/internal/app/xhttp/xhttp_test.go deleted file mode 100644 index f4fe3df0..00000000 --- a/internal/app/xhttp/xhttp_test.go +++ /dev/null @@ -1,171 +0,0 @@ -package xhttp - -import ( - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/labstack/echo/v4" - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestNonExistingEndpoint(t *testing.T) { - config, err := conf.FromEnv() - assert.Nil(t, err) - srv := New(config) - // "/" endpoint should return 404. - req := httptest.NewRequest(http.MethodGet, "/", nil) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) -} - -func TestDisableChromeEndpoints(t *testing.T) { - os.Setenv(conf.DisableGoogleChromeEnvVar, "1") - config, err := conf.FromEnv() - assert.Nil(t, err) - srv := New(config) - // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Merge endpoint should return 200. - body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // HTML endpoint should return 404. - body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // URL endpoint should return 404. - body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // Markdown endpoint should return 404. - body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // Office endpoint should return 200. - body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // finally... - os.Setenv(conf.DisableGoogleChromeEnvVar, "0") -} - -func TestDisableUnoconvEndpoints(t *testing.T) { - os.Setenv(conf.DisableUnoconvEnvVar, "1") - config, err := conf.FromEnv() - assert.Nil(t, err) - srv := New(config) - // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Merge endpoint should return 200. - body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // HTML endpoint should return 200. - body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // URL endpoint should return 200. - body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Markdown endpoint should return 200. - body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Office endpoint should return 404. - body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // finally... - os.Setenv(conf.DisableUnoconvEnvVar, "0") -} -func TestDisableChromeAndUnoconvEndpoints(t *testing.T) { - os.Setenv(conf.DisableGoogleChromeEnvVar, "1") - os.Setenv(conf.DisableUnoconvEnvVar, "1") - config, err := conf.FromEnv() - assert.Nil(t, err) - srv := New(config) - // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Merge endpoint should return 200. - body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // HTML endpoint should return 404. - body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // URL endpoint should return 404. - body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // Markdown endpoint should return 404. - body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // Office endpoint should return 404. - body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusNotFound, srv, req) - // finally... - os.Setenv(conf.DisableGoogleChromeEnvVar, "0") - os.Setenv(conf.DisableUnoconvEnvVar, "0") -} - -func TestCustomRootPath(t *testing.T) { - os.Setenv(conf.RootPathEnvVar, "/foo/") - config, err := conf.FromEnv() - assert.Nil(t, err) - srv := New(config) - // Ping endpoint should return 200. - req := httptest.NewRequest(http.MethodGet, pingEndpoint(config), nil) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Merge endpoint should return 200. - body, contentType := test.MergeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, mergeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // HTML endpoint should return 200. - body, contentType = test.HTMLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, htmlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // URL endpoint should return 200. - body, contentType = test.URLMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, urlEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Markdown endpoint should return 200. - body, contentType = test.MarkdownMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, markdownEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // Office endpoint should return 200. - body, contentType = test.OfficeMultipartForm(t, nil) - req = httptest.NewRequest(http.MethodPost, officeEndpoint(config), body) - req.Header.Set(echo.HeaderContentType, contentType) - test.AssertStatusCode(t, http.StatusOK, srv, req) - // finally... - os.Setenv(conf.RootPathEnvVar, "/") -} diff --git a/internal/pkg/chrome/chrome.go b/internal/pkg/chrome/chrome.go deleted file mode 100644 index ffbe30c1..00000000 --- a/internal/pkg/chrome/chrome.go +++ /dev/null @@ -1,186 +0,0 @@ -package chrome - -import ( - "context" - "os" - "os/exec" - "strings" - "syscall" - "time" - - "github.com/mafredri/cdp/devtool" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" -) - -// Start starts Google Chrome headless in background. -func Start(logger xlog.Logger, ignoreCertificateErrors bool) error { - const op string = "chrome.Start" - logger.DebugOp(op, "starting new Google Chrome headless process on port 9222...") - resolver := func() error { - cmd, err := cmd(logger, ignoreCertificateErrors) - if err != nil { - return err - } - // we try to start the process. - xexec.LogBeforeExecute(logger, cmd) - if err := cmd.Start(); err != nil { - return err - } - // if the process failed to start correctly, - // we have to restart it. - isViable, _ := IsViable(logger) - if !isViable { - return restart(logger, cmd.Process, ignoreCertificateErrors) - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func cmd(logger xlog.Logger, ignoreCertificateErrors bool) (*exec.Cmd, error) { - const op string = "chrome.cmd" - binary := "google-chrome-stable" - args := []string{ - "--no-sandbox", - "--headless", - // see https://github.com/thecodingmachine/gotenberg/issues/157. - "--disable-dev-shm-usage", - // See https://github.com/puppeteer/puppeteer/issues/661 - // and https://github.com/puppeteer/puppeteer/issues/2410. - "--font-render-hinting=none", - "--remote-debugging-port=9222", - "--disable-gpu", - "--disable-translate", - "--disable-extensions", - "--disable-background-networking", - "--safebrowsing-disable-auto-update", - "--disable-sync", - "--disable-default-apps", - "--hide-scrollbars", - "--metrics-recording-only", - "--mute-audio", - "--no-first-run", - } - - if ignoreCertificateErrors { - args = append(args, "--ignore-certificate-errors") - } - - cmd, err := xexec.Command(logger, binary, args...) - if err != nil { - return nil, xerror.New(op, err) - } - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - return cmd, nil -} - -func kill(logger xlog.Logger, proc *os.Process) error { - const op string = "chrome.kill" - logger.DebugOp(op, "killing Google Chrome headless process using port 9222...") - resolver := func() error { - err := syscall.Kill(-proc.Pid, syscall.SIGKILL) - if err == nil { - return nil - } - if strings.Contains(err.Error(), "no such process") { - return nil - } - return err - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func restart(logger xlog.Logger, proc *os.Process, ignoreCertificateErrors bool) error { - const op string = "chrome.restart" - logger.DebugOp(op, "restarting Google Chrome headless process using port 9222...") - resolver := func() error { - // kill the existing process first. - if err := kill(logger, proc); err != nil { - return err - } - cmd, err := cmd(logger, ignoreCertificateErrors) - if err != nil { - return err - } - // we try to restart the process. - xexec.LogBeforeExecute(logger, cmd) - if err := cmd.Start(); err != nil { - return err - } - // if the process failed to restart correctly, - // we have to restart it again. - isViable, _ := IsViable(logger) - if !isViable { - return restart(logger, cmd.Process, ignoreCertificateErrors) - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// IsViable checks if Google Chrome is healthy. -func IsViable(logger xlog.Logger) (bool, error) { - const ( - op string = "chrome.IsViable" - maxViabilityTests int = 20 - ) - viable := func() (bool, error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - endpoint := "http://localhost:9222" - logger.DebugOpf( - op, - "checking Google Chrome headless process viability via endpoint '%s/json/version'", - endpoint, - ) - v, err := devtool.New(endpoint).Version(ctx) - if err != nil { - logger.DebugOpf( - op, - "Google Chrome headless is not viable as endpoint returned '%v'", - err.Error(), - ) - return false, err - } - logger.DebugOpf( - op, - "Google Chrome headless is viable as endpoint returned '%v'", - v, - ) - return true, nil - } - result := false - var err error - - for i := 0; i < maxViabilityTests && !result; i++ { - warmup(logger) - result, err = viable() - } - return result, err -} - -func warmup(logger xlog.Logger) { - const ( - op string = "chrome.warmup" - seconds float64 = 0.5 - ) - warmupTime := xtime.Duration(seconds) - logger.DebugOpf( - op, - "waiting '%v' for allowing Google Chrome to warmup", - warmupTime, - ) - time.Sleep(warmupTime) -} diff --git a/internal/pkg/chrome/doc.go b/internal/pkg/chrome/doc.go deleted file mode 100644 index c5431149..00000000 --- a/internal/pkg/chrome/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package chrome helps starting -// Google Chrome headless in background. -package chrome diff --git a/internal/pkg/conf/conf.go b/internal/pkg/conf/conf.go deleted file mode 100644 index 7e974309..00000000 --- a/internal/pkg/conf/conf.go +++ /dev/null @@ -1,293 +0,0 @@ -package conf - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xassert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -const ( - // MaximumWaitTimeoutEnvVar contains the name - // of the environment variable "MAXIMUM_WAIT_TIMEOUT". - MaximumWaitTimeoutEnvVar string = "MAXIMUM_WAIT_TIMEOUT" - // MaximumWaitDelayEnvVar contains the name - // of the environment variable "MAXIMUM_WAIT_DELAY". - MaximumWaitDelayEnvVar string = "MAXIMUM_WAIT_DELAY" - // MaximumWebhookURLTimeoutEnvVar contains the name - // of the environment variable "MAXIMUM_WEBHOOK_URL_TIMEOUT". - MaximumWebhookURLTimeoutEnvVar string = "MAXIMUM_WEBHOOK_URL_TIMEOUT" - // DefaultWaitTimeoutEnvVar contains the name - // of the environment variable "DEFAULT_WAIT_TIMEOUT". - DefaultWaitTimeoutEnvVar string = "DEFAULT_WAIT_TIMEOUT" - // DefaultWebhookURLTimeoutEnvVar contains the name - // of the environment variable "DEFAULT_WEBHOOK_URL_TIMEOUT". - DefaultWebhookURLTimeoutEnvVar string = "DEFAULT_WEBHOOK_URL_TIMEOUT" - // DefaultListenPortEnvVar contains the name - // of the environment variable "DEFAULT_LISTEN_PORT". - DefaultListenPortEnvVar string = "DEFAULT_LISTEN_PORT" - // DisableGoogleChromeEnvVar contains the name - // of the environment variable "DISABLE_GOOGLE_CHROME". - DisableGoogleChromeEnvVar string = "DISABLE_GOOGLE_CHROME" - // DisableUnoconvEnvVar contains the name - // of the environment variable "DISABLE_UNOCONV". - DisableUnoconvEnvVar string = "DISABLE_UNOCONV" - // LogLevelEnvVar contains the name - // of the environment variable "LOG_LEVEL". - LogLevelEnvVar string = "LOG_LEVEL" - // RootPathEnvVar contains the name - // of the environment variable "ROOT_PATH". - RootPathEnvVar string = "ROOT_PATH" - // DefaultGoogleChromeRpccBufferSizeEnvVar contains the name - // of the environment variable "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE". - DefaultGoogleChromeRpccBufferSizeEnvVar string = "DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE" - // GoogleChromeIgnoreCertificateErrorsEnvVar contains the name - // of the environment variable "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS". - GoogleChromeIgnoreCertificateErrorsEnvVar string = "GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS" -) - -// Config contains the application -// configuration. -type Config struct { - maximumWaitTimeout float64 - maximumWaitDelay float64 - maximumWebhookURLTimeout float64 - defaultWaitTimeout float64 - defaultWebhookURLTimeout float64 - defaultListenPort int64 - disableGoogleChrome bool - disableUnoconv bool - googleChromeIgnoreCertificateErrors bool - logLevel xlog.Level - rootPath string - maximumGoogleChromeRpccBufferSize int64 - defaultGoogleChromeRpccBufferSize int64 -} - -// DefaultConfig returns the default -// configuration. -func DefaultConfig() Config { - return Config{ - maximumWaitTimeout: 30.0, - maximumWaitDelay: 10.0, - maximumWebhookURLTimeout: 30.0, - defaultWaitTimeout: 10.0, - defaultWebhookURLTimeout: 10.0, - defaultListenPort: 3000, - disableGoogleChrome: false, - disableUnoconv: false, - logLevel: xlog.InfoLevel, - rootPath: "/", - maximumGoogleChromeRpccBufferSize: 104857600, // ~100 MB - defaultGoogleChromeRpccBufferSize: 1048576, // 1 MB - googleChromeIgnoreCertificateErrors: false, - } -} - -/* -FromEnv returns a Conf according -to environment variables. -*/ -func FromEnv() (Config, error) { - const op string = "conf.FromEnv" - resolver := func() (Config, error) { - c := DefaultConfig() - maximumWaitTimeout, err := xassert.Float64FromEnv( - MaximumWaitTimeoutEnvVar, - c.maximumWaitTimeout, - xassert.Float64NotInferiorTo(0.0), - ) - c.maximumWaitTimeout = maximumWaitTimeout - if err != nil { - return c, err - } - maximumWaitDelay, err := xassert.Float64FromEnv( - MaximumWaitDelayEnvVar, - c.maximumWaitDelay, - xassert.Float64NotInferiorTo(0.0), - ) - c.maximumWaitDelay = maximumWaitDelay - if err != nil { - return c, err - } - maximumWebhookURLTimeout, err := xassert.Float64FromEnv( - MaximumWebhookURLTimeoutEnvVar, - c.maximumWebhookURLTimeout, - xassert.Float64NotInferiorTo(0.0), - ) - c.maximumWebhookURLTimeout = maximumWebhookURLTimeout - if err != nil { - return c, err - } - defaultWaitTimeout, err := xassert.Float64FromEnv( - DefaultWaitTimeoutEnvVar, - c.defaultWaitTimeout, - xassert.Float64NotInferiorTo(0.0), - xassert.Float64NotSuperiorTo(c.maximumWaitTimeout), - ) - c.defaultWaitTimeout = defaultWaitTimeout - if err != nil { - return c, err - } - defaultWebhookURLTimeout, err := xassert.Float64FromEnv( - DefaultWebhookURLTimeoutEnvVar, - c.defaultWebhookURLTimeout, - xassert.Float64NotInferiorTo(0.0), - xassert.Float64NotSuperiorTo(c.defaultWebhookURLTimeout), - ) - c.defaultWebhookURLTimeout = defaultWebhookURLTimeout - if err != nil { - return c, err - } - defaultListenPort, err := xassert.Int64FromEnv( - DefaultListenPortEnvVar, - c.defaultListenPort, - xassert.Int64NotInferiorTo(0), - xassert.Int64NotSuperiorTo(65535), - ) - c.defaultListenPort = defaultListenPort - if err != nil { - return c, err - } - disableGoogleChrome, err := xassert.BoolFromEnv( - DisableGoogleChromeEnvVar, - c.disableGoogleChrome, - ) - c.disableGoogleChrome = disableGoogleChrome - if err != nil { - return c, err - } - disableUnoconv, err := xassert.BoolFromEnv( - DisableUnoconvEnvVar, - c.disableUnoconv, - ) - c.disableUnoconv = disableUnoconv - if err != nil { - return c, err - } - logLevel, err := xassert.StringFromEnv( - LogLevelEnvVar, - string(c.logLevel), - xassert.StringOneOf(xlog.Levels()), - ) - c.logLevel = xlog.MustParseLevel(logLevel) - if err != nil { - return c, err - } - rootPath, err := xassert.StringFromEnv( - RootPathEnvVar, - c.rootPath, - xassert.StringStartWith("/"), - xassert.StringEndWith("/"), - ) - c.rootPath = rootPath - if err != nil { - return c, err - } - defaultGoogleChromeRpccBufferSize, err := xassert.Int64FromEnv( - DefaultGoogleChromeRpccBufferSizeEnvVar, - c.defaultGoogleChromeRpccBufferSize, - xassert.Int64NotInferiorTo(0), - xassert.Int64NotSuperiorTo(c.MaximumGoogleChromeRpccBufferSize()), - ) - c.defaultGoogleChromeRpccBufferSize = defaultGoogleChromeRpccBufferSize - if err != nil { - return c, err - } - googleChromeIgnoreCertificateErrors, err := xassert.BoolFromEnv( - GoogleChromeIgnoreCertificateErrorsEnvVar, - c.googleChromeIgnoreCertificateErrors, - ) - c.googleChromeIgnoreCertificateErrors = googleChromeIgnoreCertificateErrors - if err != nil { - return c, err - } - return c, nil - } - result, err := resolver() - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -// MaximumWaitTimeout returns the maximum -// wait timeout from the configuration. -func (c Config) MaximumWaitTimeout() float64 { - return c.maximumWaitTimeout -} - -// MaximumWaitDelay returns the maximum -// wait timeout from the configuration. -func (c Config) MaximumWaitDelay() float64 { - return c.maximumWaitDelay -} - -// MaximumWebhookURLTimeout returns the maximum -// webhook URL wait timeout from the configuration. -func (c Config) MaximumWebhookURLTimeout() float64 { - return c.maximumWebhookURLTimeout -} - -// DefaultWaitTimeout returns the default -// wait timeout from the configuration. -func (c Config) DefaultWaitTimeout() float64 { - return c.defaultWaitTimeout -} - -// DefaultWebhookURLTimeout returns the default -// webhook URL wait timeout from the configuration. -func (c Config) DefaultWebhookURLTimeout() float64 { - return c.defaultWebhookURLTimeout -} - -// DefaultListenPort returns the default -// listen port from the configuration. -func (c Config) DefaultListenPort() int64 { - return c.defaultListenPort -} - -/* -DisableGoogleChrome returns true if -Google Chrome is disabled in the -configuration. -*/ -func (c Config) DisableGoogleChrome() bool { - return c.disableGoogleChrome -} - -/* -DisableUnoconv returns true if -Unoconv is disabled in the -configuration. -*/ -func (c Config) DisableUnoconv() bool { - return c.disableUnoconv -} - -// LogLevel returns the xlog.Level from -// the configuration. -func (c Config) LogLevel() xlog.Level { - return c.logLevel -} - -// RootPath returns the rooth path from -// the configuration. -func (c Config) RootPath() string { - return c.rootPath -} - -// MaximumGoogleChromeRpccBufferSize returns the maximum -// Google Chrome rpcc buffer size from the configuration. -func (c Config) MaximumGoogleChromeRpccBufferSize() int64 { - return c.maximumGoogleChromeRpccBufferSize -} - -// DefaultGoogleChromeRpccBufferSize returns the default -// Google Chrome rpcc buffer size from the configuration. -func (c Config) DefaultGoogleChromeRpccBufferSize() int64 { - return c.defaultGoogleChromeRpccBufferSize -} - -func (c Config) GoogleChromeIgnoreCertificateErrors() bool { - return c.googleChromeIgnoreCertificateErrors -} diff --git a/internal/pkg/conf/conf_test.go b/internal/pkg/conf/conf_test.go deleted file mode 100644 index a8d2e6b4..00000000 --- a/internal/pkg/conf/conf_test.go +++ /dev/null @@ -1,435 +0,0 @@ -package conf - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestEmptyFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // no environment variables set, - // values should be equal to default config. - expected = DefaultConfig() - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) -} - -func TestMaximumWaitTimeoutFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // MAXIMUM_WAIT_TIMEOUT correctly set. - os.Setenv(MaximumWaitTimeoutEnvVar, "10.0") - expected = DefaultConfig() - expected.maximumWaitTimeout = 10.0 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitTimeoutEnvVar) - // MAXIMUM_WAIT_TIMEOUT wrongly set. - os.Setenv(MaximumWaitTimeoutEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitTimeoutEnvVar) - // MAXIMUM_WAIT_TIMEOUT < 0. - os.Setenv(MaximumWaitTimeoutEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitTimeoutEnvVar) -} - -func TestMaximumWaitDelayFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // MAXIMUM_WAIT_DELAY correctly set. - os.Setenv(MaximumWaitDelayEnvVar, "10.0") - expected = DefaultConfig() - expected.maximumWaitDelay = 10.0 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitDelayEnvVar) - // MAXIMUM_WAIT_DELAY wrongly set. - os.Setenv(MaximumWaitDelayEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitDelayEnvVar) - // MAXIMUM_WAIT_DELAY < 0. - os.Setenv(MaximumWaitDelayEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWaitDelayEnvVar) -} - -func TestMaximumWebhookURLTimeoutFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // MAXIMUM_WEBHOOK_URL_TIMEOUT correctly set. - os.Setenv(MaximumWebhookURLTimeoutEnvVar, "10.0") - expected = DefaultConfig() - expected.maximumWebhookURLTimeout = 10.0 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWebhookURLTimeoutEnvVar) - // MAXIMUM_WEBHOOK_URL_TIMEOUT wrongly set. - os.Setenv(MaximumWebhookURLTimeoutEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWebhookURLTimeoutEnvVar) - // MAXIMUM_WEBHOOK_URL_TIMEOUT < 0. - os.Setenv(MaximumWebhookURLTimeoutEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(MaximumWebhookURLTimeoutEnvVar) -} - -func TestDefaultWaitTimeoutFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DEFAULT_WAIT_TIMEOUT correctly set. - os.Setenv(DefaultWaitTimeoutEnvVar, "10.0") - expected = DefaultConfig() - expected.defaultWaitTimeout = 10.0 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWaitTimeoutEnvVar) - // DEFAULT_WAIT_TIMEOUT wrongly set. - os.Setenv(DefaultWaitTimeoutEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWaitTimeoutEnvVar) - // DEFAULT_WAIT_TIMEOUT < 0. - os.Setenv(DefaultWaitTimeoutEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWaitTimeoutEnvVar) - // DEFAULT_WAIT_TIMEOUT > MAXIMUM_WAIT_TIMEOUT. - os.Setenv(DefaultWaitTimeoutEnvVar, "40.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWaitTimeoutEnvVar) -} - -func TestDefaultWebhookURLTimeoutFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DEFAULT_WEBHOOK_URL_TIMEOUT correctly set. - os.Setenv(DefaultWebhookURLTimeoutEnvVar, "10.0") - expected = DefaultConfig() - expected.defaultWebhookURLTimeout = 10.0 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWebhookURLTimeoutEnvVar) - // DEFAULT_WEBHOOK_URL_TIMEOUT wrongly set. - os.Setenv(DefaultWebhookURLTimeoutEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWebhookURLTimeoutEnvVar) - // DEFAULT_WEBHOOK_URL_TIMEOUT < 0. - os.Setenv(DefaultWebhookURLTimeoutEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWebhookURLTimeoutEnvVar) - // DEFAULT_WEBHOOK_URL_TIMEOUT > MAXIMUM_WEBHOOK_URL_TIMEOUT. - os.Setenv(DefaultWebhookURLTimeoutEnvVar, "40.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultWebhookURLTimeoutEnvVar) -} - -func TestDefaultListenPortFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DEFAULT_LISTEN_PORT correctly set. - os.Setenv(DefaultListenPortEnvVar, "80") - expected = DefaultConfig() - expected.defaultListenPort = 80 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultListenPortEnvVar) - // DEFAULT_LISTEN_PORT wrongly set. - os.Setenv(DefaultListenPortEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultListenPortEnvVar) - // DEFAULT_LISTEN_PORT < 0. - os.Setenv(DefaultListenPortEnvVar, "-1.0") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultListenPortEnvVar) - // DEFAULT_LISTEN_PORT > 65535. - os.Setenv(DefaultListenPortEnvVar, "65536") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultListenPortEnvVar) -} - -func TestDisableGoogleChromeFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DISABLE_GOOGLE_CHROME correctly set. - os.Setenv(DisableGoogleChromeEnvVar, "1") - expected = DefaultConfig() - expected.disableGoogleChrome = true - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableGoogleChromeEnvVar) - os.Setenv(DisableGoogleChromeEnvVar, "0") - expected = DefaultConfig() - expected.disableGoogleChrome = false - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableGoogleChromeEnvVar) - // DISABLE_GOOGLE_CHROME wrongly set. - os.Setenv(DisableGoogleChromeEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableGoogleChromeEnvVar) -} - -func TestDisableUnoconvFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DISABLE_UNOCONV correctly set. - os.Setenv(DisableUnoconvEnvVar, "1") - expected = DefaultConfig() - expected.disableUnoconv = true - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableUnoconvEnvVar) - os.Setenv(DisableUnoconvEnvVar, "0") - expected = DefaultConfig() - expected.disableUnoconv = false - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableUnoconvEnvVar) - // DISABLE_UNOCONV wrongly set. - os.Setenv(DisableUnoconvEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DisableUnoconvEnvVar) -} - -func TestLogLevelFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // LOG_LEVEL correctly set. - os.Setenv(LogLevelEnvVar, "DEBUG") - expected = DefaultConfig() - expected.logLevel = xlog.DebugLevel - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(LogLevelEnvVar) - os.Setenv(LogLevelEnvVar, "INFO") - expected = DefaultConfig() - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(LogLevelEnvVar) - os.Setenv(LogLevelEnvVar, "ERROR") - expected = DefaultConfig() - expected.logLevel = xlog.ErrorLevel - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(LogLevelEnvVar) - // LOG_LEVEL wrongly set. - os.Setenv(LogLevelEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(LogLevelEnvVar) -} - -func TestRootPathFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // ROOT_PATH correctly set. - os.Setenv(RootPathEnvVar, "/foo/") - expected = DefaultConfig() - expected.rootPath = "/foo/" - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(RootPathEnvVar) - // ROOT_PATH wrongly set. - os.Setenv(RootPathEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(RootPathEnvVar) -} - -func TestDefaultGoogleChromeRpccBufferSizeFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE correctly set. - os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "100") - expected = DefaultConfig() - expected.defaultGoogleChromeRpccBufferSize = 100 - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar) - // DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE wrongly set. - os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar) - // DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE < 0. - os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "-1") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar) - // DEFAULT_GOOGLE_CHROME_RPCC_BUFFER_SIZE > 100 MB (maximumGoogleChromeRpccBufferSize). - os.Setenv(DefaultGoogleChromeRpccBufferSizeEnvVar, "104857601") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(DefaultGoogleChromeRpccBufferSizeEnvVar) -} - -func TestGoogleChromeIgnoreCertificateErrorsFromEnv(t *testing.T) { - var ( - expected Config - result Config - err error - ) - // GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to true. - os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "1") - expected = DefaultConfig() - expected.googleChromeIgnoreCertificateErrors = true - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar) - // GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS correctly set to false. - os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "0") - expected = DefaultConfig() - expected.googleChromeIgnoreCertificateErrors = false - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar) - // GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS wrongly set. - os.Setenv(GoogleChromeIgnoreCertificateErrorsEnvVar, "foo") - expected = DefaultConfig() - result, err = FromEnv() - test.AssertError(t, err) - assert.Equal(t, expected, result) - os.Unsetenv(GoogleChromeIgnoreCertificateErrorsEnvVar) - // GOOGLE_CHROME_IGNORE_CERTIFICATE_ERRORS not set at all. - expected = DefaultConfig() - expected.googleChromeIgnoreCertificateErrors = false - result, err = FromEnv() - assert.Nil(t, err) - assert.Equal(t, expected, result) -} - -func TestGetters(t *testing.T) { - result := DefaultConfig() - assert.Equal(t, result.maximumWaitTimeout, result.MaximumWaitTimeout()) - assert.Equal(t, result.maximumWaitDelay, result.MaximumWaitDelay()) - assert.Equal(t, result.maximumWebhookURLTimeout, result.MaximumWebhookURLTimeout()) - assert.Equal(t, result.defaultWaitTimeout, result.DefaultWaitTimeout()) - assert.Equal(t, result.defaultWebhookURLTimeout, result.DefaultWebhookURLTimeout()) - assert.Equal(t, result.defaultListenPort, result.DefaultListenPort()) - assert.Equal(t, result.disableGoogleChrome, result.DisableGoogleChrome()) - assert.Equal(t, result.disableUnoconv, result.DisableUnoconv()) - assert.Equal(t, result.logLevel, result.LogLevel()) - assert.Equal(t, result.rootPath, result.RootPath()) - assert.Equal(t, result.maximumGoogleChromeRpccBufferSize, result.MaximumGoogleChromeRpccBufferSize()) - assert.Equal(t, result.defaultGoogleChromeRpccBufferSize, result.DefaultGoogleChromeRpccBufferSize()) - assert.Equal(t, result.googleChromeIgnoreCertificateErrors, result.GoogleChromeIgnoreCertificateErrors()) -} diff --git a/internal/pkg/conf/doc.go b/internal/pkg/conf/doc.go deleted file mode 100644 index 352ce083..00000000 --- a/internal/pkg/conf/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package conf gathers all -// configuration data. -package conf diff --git a/internal/pkg/normalize/doc.go b/internal/pkg/normalize/doc.go deleted file mode 100644 index f5e6f4ab..00000000 --- a/internal/pkg/normalize/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -/* -Package normalize helps removing special -characters from a string. - -Fixes: https://github.com/thecodingmachine/gotenberg/issues/104 - -Credits: https://medium.com/@swdream/golang-remove-all-accents-in-string-319abf6a7f5b -*/ -package normalize diff --git a/internal/pkg/normalize/normalize.go b/internal/pkg/normalize/normalize.go deleted file mode 100644 index 5b58c55b..00000000 --- a/internal/pkg/normalize/normalize.go +++ /dev/null @@ -1,21 +0,0 @@ -package normalize - -import ( - "unicode" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "golang.org/x/text/runes" - "golang.org/x/text/transform" - "golang.org/x/text/unicode/norm" -) - -// String normalizes given string. -func String(str string) (string, error) { - const op string = "normalize.String" - t := transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC) - result, _, err := transform.String(t, str) - if err != nil { - return "", xerror.New(op, err) - } - return result, nil -} diff --git a/internal/pkg/normalize/normalize_test.go b/internal/pkg/normalize/normalize_test.go deleted file mode 100644 index 087ad509..00000000 --- a/internal/pkg/normalize/normalize_test.go +++ /dev/null @@ -1,17 +0,0 @@ -package normalize - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestString(t *testing.T) { - const ( - toNormalize string = "analise da aplicação" - expected string = "analise da aplicacao" - ) - v, err := String(toNormalize) - assert.Nil(t, err) - assert.Equal(t, expected, v) -} diff --git a/internal/pkg/printer/chrome.go b/internal/pkg/printer/chrome.go deleted file mode 100644 index 5fe3ca73..00000000 --- a/internal/pkg/printer/chrome.go +++ /dev/null @@ -1,397 +0,0 @@ -package printer - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "strings" - "time" - - "github.com/mafredri/cdp" - "github.com/mafredri/cdp/devtool" - "github.com/mafredri/cdp/protocol/network" - "github.com/mafredri/cdp/protocol/page" - "github.com/mafredri/cdp/protocol/target" - "github.com/mafredri/cdp/rpcc" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" - "golang.org/x/sync/errgroup" -) - -type chromePrinter struct { - logger xlog.Logger - url string - opts ChromePrinterOptions -} - -// ChromePrinterOptions helps customizing the -// Google Chrome Printer behaviour. -type ChromePrinterOptions struct { - WaitTimeout float64 - WaitDelay float64 - HeaderHTML string - FooterHTML string - PaperWidth float64 - PaperHeight float64 - MarginTop float64 - MarginBottom float64 - MarginLeft float64 - MarginRight float64 - Landscape bool - PageRanges string - RpccBufferSize int64 - CustomHTTPHeaders map[string]string - Scale float64 -} - -// DefaultChromePrinterOptions returns the default -// Google Chrome Printer options. -func DefaultChromePrinterOptions(config conf.Config) ChromePrinterOptions { - const defaultHeaderFooterHTML string = "" - return ChromePrinterOptions{ - WaitTimeout: config.DefaultWaitTimeout(), - WaitDelay: 0.0, - HeaderHTML: defaultHeaderFooterHTML, - FooterHTML: defaultHeaderFooterHTML, - PaperWidth: 8.27, - PaperHeight: 11.7, - MarginTop: 1.0, - MarginBottom: 1.0, - MarginLeft: 1.0, - MarginRight: 1.0, - Landscape: false, - PageRanges: "", - RpccBufferSize: config.DefaultGoogleChromeRpccBufferSize(), - CustomHTTPHeaders: make(map[string]string), - Scale: 1.0, - } -} - -// nolint: gochecknoglobals -var lockChrome = make(chan struct{}, 1) - -const maxDevtConnections int = 5 - -// nolint: gochecknoglobals -var devtConnections int - -func (p chromePrinter) Print(destination string) error { - const op string = "printer.chromePrinter.Print" - logOptions(p.logger, p.opts) - ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout+p.opts.WaitDelay) - defer cancel() - resolver := func() error { - devt, err := devtool.New("http://localhost:9222").Version(ctx) - if err != nil { - return err - } - // connect to WebSocket URL (page) that speaks the Chrome DevTools Protocol. - devtConn, err := rpcc.DialContext(ctx, devt.WebSocketDebuggerURL) - if err != nil { - return err - } - defer devtConn.Close() - // create a new CDP Client that uses conn. - devtClient := cdp.NewClient(devtConn) - createBrowserContextArgs := target.NewCreateBrowserContextArgs() - newContextTarget, err := devtClient.Target.CreateBrowserContext(ctx, createBrowserContextArgs) - if err != nil { - return err - } - /* - close the browser context when done. - we're not using the "default" context - as it may timeout before actually closing - the browser context. - see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670 - */ - disposeBrowserContextArgs := target.NewDisposeBrowserContextArgs(newContextTarget.BrowserContextID) - defer devtClient.Target.DisposeBrowserContext(context.Background(), disposeBrowserContextArgs) // nolint: errcheck - // create a new blank target with the new browser context. - createTargetArgs := target. - NewCreateTargetArgs("about:blank"). - SetBrowserContextID(newContextTarget.BrowserContextID) - newTarget, err := devtClient.Target.CreateTarget(ctx, createTargetArgs) - if err != nil { - return err - } - // connect the client to the new target. - newTargetWsURL := fmt.Sprintf("ws://127.0.0.1:9222/devtools/page/%s", newTarget.TargetID) - newContextConn, err := rpcc.DialContext( - ctx, - newTargetWsURL, - /* - see: - https://github.com/thecodingmachine/gotenberg/issues/108 - https://github.com/mafredri/cdp/issues/4 - https://github.com/ChromeDevTools/devtools-protocol/issues/24 - */ - rpcc.WithWriteBufferSize(int(p.opts.RpccBufferSize)), - rpcc.WithCompression(), - ) - if err != nil { - return err - } - defer newContextConn.Close() - // create a new CDP Client that uses newContextConn. - targetClient := cdp.NewClient(newContextConn) - /* - close the target when done. - we're not using the "default" context - as it may timeout before actually closing - the target. - see: https://github.com/mafredri/cdp/issues/101#issuecomment-524533670 - */ - closeTargetArgs := target.NewCloseTargetArgs(newTarget.TargetID) - defer targetClient.Target.CloseTarget(context.Background(), closeTargetArgs) // nolint: errcheck - // enable all events. - if err := p.enableEvents(ctx, targetClient); err != nil { - return err - } - // add custom headers (if any). - if err := p.setCustomHTTPHeaders(ctx, targetClient); err != nil { - return err - } - // listen for all events. - if err := p.listenEvents(ctx, targetClient); err != nil { - return err - } - // apply a wait delay (if any). - if p.opts.WaitDelay > 0.0 { - // wait for a given amount of time (useful for javascript delay). - p.logger.DebugOpf(op, "applying a wait delay of '%.2fs'...", p.opts.WaitDelay) - time.Sleep(xtime.Duration(p.opts.WaitDelay)) - } else { - p.logger.DebugOp(op, "no wait delay to apply, moving on...") - } - printToPdfArgs := page.NewPrintToPDFArgs(). - SetPaperWidth(p.opts.PaperWidth). - SetPaperHeight(p.opts.PaperHeight). - SetMarginTop(p.opts.MarginTop). - SetMarginBottom(p.opts.MarginBottom). - SetMarginLeft(p.opts.MarginLeft). - SetMarginRight(p.opts.MarginRight). - SetLandscape(p.opts.Landscape). - SetDisplayHeaderFooter(true). - SetHeaderTemplate(p.opts.HeaderHTML). - SetFooterTemplate(p.opts.FooterHTML). - SetPrintBackground(true). - SetScale(p.opts.Scale) - if p.opts.PageRanges != "" { - printToPdfArgs.SetPageRanges(p.opts.PageRanges) - } - // printToPDF the page to PDF. - printToPDF, err := targetClient.Page.PrintToPDF( - ctx, - printToPdfArgs, - ) - if err != nil { - // find a way to check it in the handlers? - if strings.Contains(err.Error(), "Page range syntax error") { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' is not a valid Google Chrome page ranges", p.opts.PageRanges), - err, - ) - } - if strings.Contains(err.Error(), "rpcc: message too large") { - return xerror.Invalid( - op, - fmt.Sprintf( - "'%d' bytes are not enough: increase the Google Chrome rpcc buffer size (up to 100 MB)", - p.opts.RpccBufferSize, - ), - err, - ) - } - return err - } - if err := ioutil.WriteFile(destination, printToPDF.Data, 0600); err != nil { - return err - } - return nil - } - if devtConnections < maxDevtConnections { - p.logger.DebugOp(op, "skipping lock acquisition...") - devtConnections++ - err := resolver() - devtConnections-- - if err != nil { - return xcontext.MustHandleError( - ctx, - xerror.New(op, err), - ) - } - return nil - } - p.logger.DebugOp(op, "waiting lock to be acquired...") - select { - case lockChrome <- struct{}{}: - // lock acquired. - p.logger.DebugOp(op, "lock acquired") - devtConnections++ - err := resolver() - devtConnections-- - <-lockChrome // we release the lock. - if err != nil { - return xcontext.MustHandleError( - ctx, - xerror.New(op, err), - ) - } - return nil - case <-ctx.Done(): - // failed to acquire lock before - // deadline. - p.logger.DebugOp(op, "failed to acquire lock before context.Context deadline") - return xcontext.MustHandleError( - ctx, - ctx.Err(), - ) - } -} - -func (p chromePrinter) enableEvents(ctx context.Context, client *cdp.Client) error { - const op string = "printer.chromePrinter.enableEvents" - // enable all the domain events that we're interested in. - if err := runBatch( - func() error { return client.DOM.Enable(ctx) }, - func() error { return client.Network.Enable(ctx, network.NewEnableArgs()) }, - func() error { return client.Page.Enable(ctx) }, - func() error { - return client.Page.SetLifecycleEventsEnabled(ctx, page.NewSetLifecycleEventsEnabledArgs(true)) - }, - func() error { return client.Runtime.Enable(ctx) }, - ); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p chromePrinter) setCustomHTTPHeaders(ctx context.Context, client *cdp.Client) error { - const op string = "printer.chromePrinter.setCustomHTTPHeaders" - resolver := func() error { - if len(p.opts.CustomHTTPHeaders) == 0 { - p.logger.DebugOp(op, "skipping custom HTTP headers as none have been provided...") - return nil - } - customHTTPHeaders := make(map[string]string) - // useless but for the logs. - for key, value := range p.opts.CustomHTTPHeaders { - customHTTPHeaders[key] = value - p.logger.DebugOpf(op, "set '%s' to custom HTTP header '%s'", value, key) - } - b, err := json.Marshal(customHTTPHeaders) - if err != nil { - return err - } - // should always be called after client.Network.Enable. - return client.Network.SetExtraHTTPHeaders(ctx, network.NewSetExtraHTTPHeadersArgs(b)) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func (p chromePrinter) listenEvents(ctx context.Context, client *cdp.Client) error { - const op string = "printer.chromePrinter.listenEvents" - resolver := func() error { - // make sure Page events are enabled. - if err := client.Page.Enable(ctx); err != nil { - return err - } - // make sure Network events are enabled. - if err := client.Network.Enable(ctx, nil); err != nil { - return err - } - // create all clients for events. - domContentEventFired, err := client.Page.DOMContentEventFired(ctx) - if err != nil { - return err - } - defer domContentEventFired.Close() - loadEventFired, err := client.Page.LoadEventFired(ctx) - if err != nil { - return err - } - defer loadEventFired.Close() - lifecycleEvent, err := client.Page.LifecycleEvent(ctx) - if err != nil { - return err - } - defer lifecycleEvent.Close() - loadingFinished, err := client.Network.LoadingFinished(ctx) - if err != nil { - return err - } - defer loadingFinished.Close() - if _, err := client.Page.Navigate(ctx, page.NewNavigateArgs(p.url)); err != nil { - return err - } - // wait for all events. - return runBatch( - func() error { - _, err := domContentEventFired.Recv() - if err != nil { - return err - } - p.logger.DebugOp(op, "event 'domContentEventFired' received") - return nil - }, - func() error { - _, err := loadEventFired.Recv() - if err != nil { - return err - } - p.logger.DebugOp(op, "event 'loadEventFired' received") - return nil - }, - func() error { - const networkIdleEventName string = "networkIdle" - for { - ev, err := lifecycleEvent.Recv() - if err != nil { - return err - } - p.logger.DebugOpf(op, "event '%s' received", ev.Name) - if ev.Name == networkIdleEventName { - break - } - } - return nil - }, - func() error { - _, err := loadingFinished.Recv() - if err != nil { - return err - } - p.logger.DebugOp(op, "event 'loadingFinished' received") - return nil - }, - ) - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func runBatch(fn ...func() error) error { - // run all functions simultaneously and wait until - // execution has completed or an error is encountered. - eg := errgroup.Group{} - for _, f := range fn { - eg.Go(f) - } - return eg.Wait() -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Printer(new(chromePrinter)) -) diff --git a/internal/pkg/printer/doc.go b/internal/pkg/printer/doc.go deleted file mode 100644 index d36329e3..00000000 --- a/internal/pkg/printer/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package printer helps converting -// a specific file type to PDF. -package printer diff --git a/internal/pkg/printer/html.go b/internal/pkg/printer/html.go deleted file mode 100644 index edad5ebf..00000000 --- a/internal/pkg/printer/html.go +++ /dev/null @@ -1,18 +0,0 @@ -package printer - -import ( - "fmt" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// NewHTMLPrinter returns a Printer which -// is able to convert an HTML file to PDF. -func NewHTMLPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) Printer { - URL := fmt.Sprintf("file://%s", fpath) - return chromePrinter{ - logger: logger, - url: URL, - opts: opts, - } -} diff --git a/internal/pkg/printer/html_test.go b/internal/pkg/printer/html_test.go deleted file mode 100644 index e69539fa..00000000 --- a/internal/pkg/printer/html_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package printer - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestHTMLPrinter(t *testing.T) { - var ( - logger xlog.Logger = test.DebugLogger() - config conf.Config = conf.DefaultConfig() - fpath string = test.HTMLFpaths(t)[0] - opts ChromePrinterOptions - dest string - p Printer - err error - ) - // default options. - opts = DefaultChromePrinterOptions(config) - p = NewHTMLPrinter(logger, fpath, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a wait delay. - opts = DefaultChromePrinterOptions(config) - opts.WaitDelay = 0.5 - p = NewHTMLPrinter(logger, fpath, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a page ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "1" - p = NewHTMLPrinter(logger, fpath, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as options have - // a wrong page ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "foo" - p = NewHTMLPrinter(logger, fpath, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - opts = DefaultChromePrinterOptions(config) - opts.WaitTimeout = 0.0 - p = NewHTMLPrinter(logger, fpath, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) -} diff --git a/internal/pkg/printer/markdown.go b/internal/pkg/printer/markdown.go deleted file mode 100644 index 4bffdcce..00000000 --- a/internal/pkg/printer/markdown.go +++ /dev/null @@ -1,71 +0,0 @@ -package printer - -import ( - "bytes" - "fmt" - "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday/v2" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" - "html/template" - "io/ioutil" - "path/filepath" -) - -// NewMarkdownPrinter returns a Printer which -// is able to convert Markdown files to PDF. -func NewMarkdownPrinter(logger xlog.Logger, fpath string, opts ChromePrinterOptions) (Printer, error) { - const op string = "printer.NewMarkdownPrinter" - resolver := func() (string, error) { - tmpl, err := template. - New(filepath.Base(fpath)). - Funcs(template.FuncMap{"toHTML": markdownToHTML}). - ParseFiles(fpath) - if err != nil { - return "", err - } - dirPath := filepath.Dir(fpath) - data := &templateData{DirPath: dirPath} - logger.DebugOp(op, "converting Markdown files to HTML...") - var buffer bytes.Buffer - if err := tmpl.Execute(&buffer, data); err != nil { - return "", err - } - baseFilename := xrand.Get() - dst := fmt.Sprintf("%s/%s.html", dirPath, baseFilename) - logger.DebugOp(op, "writing the HTML from previous conversion(s) into new file...") - if err := ioutil.WriteFile(dst, buffer.Bytes(), 0600); err != nil { - return "", err - } - return fmt.Sprintf("file://%s", dst), nil - } - URL, err := resolver() - if err != nil { - return chromePrinter{}, xerror.New(op, err) - } - return chromePrinter{ - logger: logger, - url: URL, - opts: opts, - }, nil -} - -type templateData struct { - DirPath string -} - -func markdownToHTML(dirPath, filename string) (template.HTML, error) { - const op string = "printer.markdownToHTML" - // avoid directory traversal. - filename = filepath.Base(filename) - fpath := fmt.Sprintf("%s/%s", dirPath, filename) - b, err := ioutil.ReadFile(fpath) - if err != nil { - return "", xerror.New(op, err) - } - unsafe := blackfriday.Run(b) - content := bluemonday.UGCPolicy().SanitizeBytes(unsafe) - /* #nosec */ - return template.HTML(content), nil -} diff --git a/internal/pkg/printer/markdown_test.go b/internal/pkg/printer/markdown_test.go deleted file mode 100644 index 9627cdea..00000000 --- a/internal/pkg/printer/markdown_test.go +++ /dev/null @@ -1,77 +0,0 @@ -package printer - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestMarkdownPrinter(t *testing.T) { - var ( - logger xlog.Logger = test.DebugLogger() - config conf.Config = conf.DefaultConfig() - fpath string = test.MarkdownFpaths(t)[0] - opts ChromePrinterOptions - dest string - p Printer - err error - ) - // default options. - opts = DefaultChromePrinterOptions(config) - p, err = NewMarkdownPrinter(logger, fpath, opts) - assert.Nil(t, err) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a wait delay. - opts = DefaultChromePrinterOptions(config) - opts.WaitDelay = 0.5 - p, err = NewMarkdownPrinter(logger, fpath, opts) - assert.Nil(t, err) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a page ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "1" - p, err = NewMarkdownPrinter(logger, fpath, opts) - assert.Nil(t, err) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as options have - // a wrong page ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "foo" - p, err = NewMarkdownPrinter(logger, fpath, opts) - assert.Nil(t, err) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - opts = DefaultChromePrinterOptions(config) - opts.WaitTimeout = 0.0 - p, err = NewMarkdownPrinter(logger, fpath, opts) - assert.Nil(t, err) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) -} diff --git a/internal/pkg/printer/merge.go b/internal/pkg/printer/merge.go deleted file mode 100644 index 6fab9a83..00000000 --- a/internal/pkg/printer/merge.go +++ /dev/null @@ -1,79 +0,0 @@ -package printer - -import ( - "context" - "sort" - - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -type mergePrinter struct { - ctx context.Context - logger xlog.Logger - fpaths []string - opts MergePrinterOptions -} - -// MergePrinterOptions helps customizing the -// merge Printer behaviour. -type MergePrinterOptions struct { - WaitTimeout float64 -} - -// DefaultMergePrinterOptions returns the default -// merge Printer options. -func DefaultMergePrinterOptions(config conf.Config) MergePrinterOptions { - return MergePrinterOptions{ - WaitTimeout: config.DefaultWaitTimeout(), - } -} - -// NewMergePrinter returns a Printer which -// is able to merge PDFs. -func NewMergePrinter(logger xlog.Logger, fpaths []string, opts MergePrinterOptions) Printer { - return mergePrinter{ - logger: logger, - fpaths: fpaths, - opts: opts, - } -} - -func (p mergePrinter) Print(destination string) error { - const op string = "printer.mergePrinter.Print" - /* - context.Context may be providen from - an officePrinter which needs to merge - its result files. - */ - if p.ctx == nil { - logOptions(p.logger, p.opts) - ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout) - defer cancel() - p.ctx = ctx - } - // see https://github.com/thecodingmachine/gotenberg/issues/139. - sort.Strings(p.fpaths) - p.logger.DebugOpf(op, "merging '%v'...", p.fpaths) - resolver := func() error { - var args []string - args = append(args, p.fpaths...) - args = append(args, "cat", "output", destination) - return xexec.Run(p.ctx, p.logger, "pdftk", args...) - } - if err := resolver(); err != nil { - return xcontext.MustHandleError( - p.ctx, - xerror.New(op, err), - ) - } - return nil -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Printer(new(mergePrinter)) -) diff --git a/internal/pkg/printer/merge_test.go b/internal/pkg/printer/merge_test.go deleted file mode 100644 index 71399a32..00000000 --- a/internal/pkg/printer/merge_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package printer - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestMergePrinter(t *testing.T) { - var ( - logger xlog.Logger = test.DebugLogger() - config conf.Config = conf.DefaultConfig() - fpaths []string = test.MergeFpaths(t) - opts MergePrinterOptions - dest string - p Printer - err error - ) - // default options. - opts = DefaultMergePrinterOptions(config) - p = NewMergePrinter(logger, fpaths, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - opts = DefaultMergePrinterOptions(config) - opts.WaitTimeout = 0.0 - p = NewMergePrinter(logger, fpaths, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) -} diff --git a/internal/pkg/printer/office.go b/internal/pkg/printer/office.go deleted file mode 100644 index 52fffa56..00000000 --- a/internal/pkg/printer/office.go +++ /dev/null @@ -1,152 +0,0 @@ -package printer - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/phayes/freeport" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xexec" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" -) - -type officePrinter struct { - logger xlog.Logger - fpaths []string - opts OfficePrinterOptions -} - -// OfficePrinterOptions helps customizing the -// Office Printer behaviour. -type OfficePrinterOptions struct { - WaitTimeout float64 - Landscape bool - PageRanges string -} - -// DefaultOfficePrinterOptions returns the default -// Office Printer options. -func DefaultOfficePrinterOptions(config conf.Config) OfficePrinterOptions { - return OfficePrinterOptions{ - WaitTimeout: config.DefaultWaitTimeout(), - Landscape: false, - PageRanges: "", - } -} - -// NewOfficePrinter returns a Printer which -// is able to convert Office documents to PDF. -func NewOfficePrinter(logger xlog.Logger, fpaths []string, opts OfficePrinterOptions) Printer { - return officePrinter{ - logger: logger, - fpaths: fpaths, - opts: opts, - } -} - -func (p officePrinter) Print(destination string) error { - const op string = "printer.officePrinter.Print" - logOptions(p.logger, p.opts) - ctx, cancel := xcontext.WithTimeout(p.logger, p.opts.WaitTimeout) - defer cancel() - resolver := func() error { - // see https://github.com/thecodingmachine/gotenberg/issues/139. - sort.Strings(p.fpaths) - fpaths := make([]string, len(p.fpaths)) - dirPath := filepath.Dir(destination) - for i, fpath := range p.fpaths { - baseFilename := xrand.Get() - tmpDest := fmt.Sprintf("%s/%d%s.pdf", dirPath, i, baseFilename) - p.logger.DebugOpf(op, "converting '%s' to PDF...", fpath) - if err := p.unoconv(ctx, fpath, tmpDest); err != nil { - return err - } - p.logger.DebugOpf(op, "'%s.pdf' created", baseFilename) - fpaths[i] = tmpDest - } - if len(fpaths) == 1 { - p.logger.DebugOp(op, "only one PDF created, nothing to merge") - return os.Rename(fpaths[0], destination) - } - m := mergePrinter{ - logger: p.logger, - ctx: ctx, - fpaths: fpaths, - } - return m.Print(destination) - } - if err := resolver(); err != nil { - return xcontext.MustHandleError( - ctx, - xerror.New(op, err), - ) - } - return nil -} - -func (p officePrinter) unoconv(ctx context.Context, fpath, destination string) error { - const op string = "printer.unoconv" - resolver := func() error { - dirName := xrand.Get() - port, err := freeport.GetFreePort() - if err != nil { - return err - } - args := []string{ - "--user-profile", - fmt.Sprintf("///tmp/%s", dirName), - "--port", - fmt.Sprintf("%d", port), - "--format", - "pdf", - } - if p.opts.Landscape { - args = append(args, "--printer", "PaperOrientation=landscape") - } - if p.opts.PageRanges != "" { - args = append(args, "--export", fmt.Sprintf("PageRange=%s", p.opts.PageRanges)) - } - args = append(args, "--output", destination, fpath) - err = xexec.Run(ctx, p.logger, "unoconv", args...) - // always remove user profile folders created by LibreOffice. - // see https://github.com/thecodingmachine/gotenberg/issues/192. - go cleanupUserProfile(p.logger, dirName) - if err != nil { - // find a way to check it in the handlers? - if p.opts.PageRanges != "" && strings.Contains(err.Error(), "exit status 5") { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' is not a valid LibreOffice page ranges", p.opts.PageRanges), - err, - ) - } - return err - } - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func cleanupUserProfile(logger xlog.Logger, dirName string) { - const op = "printer.cleanupUserProfile" - path := fmt.Sprintf("/tmp/%s", dirName) - if err := os.RemoveAll(path); err != nil { - // find a way to bubble up this error? - logger.ErrorOpf(op, "failed to remove user profile directory '%s': %s", path, err.Error()) - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = Printer(new(officePrinter)) -) diff --git a/internal/pkg/printer/office_test.go b/internal/pkg/printer/office_test.go deleted file mode 100644 index 5eb8992e..00000000 --- a/internal/pkg/printer/office_test.go +++ /dev/null @@ -1,80 +0,0 @@ -package printer - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestOfficePrinter(t *testing.T) { - var ( - logger xlog.Logger = test.DebugLogger() - config conf.Config = conf.DefaultConfig() - fpaths []string = test.OfficeFpaths(t) - opts OfficePrinterOptions - dest string - p Printer - err error - ) - // default options. - opts = DefaultOfficePrinterOptions(config) - p = NewOfficePrinter(logger, fpaths, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // using one file. - opts = DefaultOfficePrinterOptions(config) - p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with landscape. - opts = DefaultOfficePrinterOptions(config) - opts.Landscape = true - p = NewOfficePrinter(logger, fpaths, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with page ranges. - opts = DefaultOfficePrinterOptions(config) - opts.PageRanges = "1-1" - p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as options have - // a wrong page ranges. - opts = DefaultOfficePrinterOptions(config) - opts.PageRanges = "foo" - p = NewOfficePrinter(logger, []string{fpaths[0]}, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - opts = DefaultOfficePrinterOptions(config) - opts.WaitTimeout = 0.0 - p = NewOfficePrinter(logger, fpaths, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) -} diff --git a/internal/pkg/printer/printer.go b/internal/pkg/printer/printer.go deleted file mode 100644 index 97c05a93..00000000 --- a/internal/pkg/printer/printer.go +++ /dev/null @@ -1,16 +0,0 @@ -package printer - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// Printer is a type that can create a PDF file from a source. -// The source is defined in the underlying implementation. -type Printer interface { - Print(destination string) error -} - -func logOptions(logger xlog.Logger, opts interface{}) { - const op string = "printer.logOptions" - logger.DebugOpf(op, "options: %+v", opts) -} diff --git a/internal/pkg/printer/url.go b/internal/pkg/printer/url.go deleted file mode 100644 index 191fb93b..00000000 --- a/internal/pkg/printer/url.go +++ /dev/null @@ -1,15 +0,0 @@ -package printer - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// NewURLPrinter returns a Printer which -// is able to convert a URL to PDF. -func NewURLPrinter(logger xlog.Logger, url string, opts ChromePrinterOptions) Printer { - return chromePrinter{ - logger: logger, - url: url, - opts: opts, - } -} diff --git a/internal/pkg/printer/url_test.go b/internal/pkg/printer/url_test.go deleted file mode 100644 index ef477e75..00000000 --- a/internal/pkg/printer/url_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package printer - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestURLPrinter(t *testing.T) { - var ( - logger xlog.Logger = test.DebugLogger() - config conf.Config = conf.DefaultConfig() - URL = "https://google.com" - opts ChromePrinterOptions - dest string - p Printer - err error - ) - // default options. - opts = DefaultChromePrinterOptions(config) - p = NewURLPrinter(logger, URL, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a wait delay. - opts = DefaultChromePrinterOptions(config) - opts.WaitDelay = 0.5 - p = NewURLPrinter(logger, URL, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // options with a pages ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "1" - p = NewURLPrinter(logger, URL, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - assert.Nil(t, err) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as options have - // a wrong page ranges. - opts = DefaultChromePrinterOptions(config) - opts.PageRanges = "foo" - p = NewURLPrinter(logger, URL, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.InvalidCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - opts = DefaultChromePrinterOptions(config) - opts.WaitTimeout = 0.0 - p = NewURLPrinter(logger, URL, opts) - dest = test.GenerateDestination() - err = p.Print(dest) - test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(err)) - err = os.RemoveAll(dest) - assert.Nil(t, err) -} diff --git a/internal/pkg/xassert/doc.go b/internal/pkg/xassert/doc.go deleted file mode 100644 index 14b2417e..00000000 --- a/internal/pkg/xassert/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -/* -Package xassert is a helper for converting -and/or validating strings. - -All functions return our standard xerror.Error -in case of error. -*/ -package xassert diff --git a/internal/pkg/xassert/float64.go b/internal/pkg/xassert/float64.go deleted file mode 100644 index e8521b7e..00000000 --- a/internal/pkg/xassert/float64.go +++ /dev/null @@ -1,88 +0,0 @@ -package xassert - -import ( - "fmt" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// RuleFloat64 is an interface for -// validating a float64. -type RuleFloat64 interface { - with(key string, value float64) - validate() error -} - -type baseRuleFloat64 struct { - key string - value float64 -} - -func (r *baseRuleFloat64) with(key string, value float64) { - r.key = key - r.value = value -} - -type ruleFloat64NotInferiorTo struct { - *baseRuleFloat64 - lowerBound float64 -} - -func (r ruleFloat64NotInferiorTo) validate() error { - const op string = "xassert.ruleFloat64NotInferiorTo.validate" - if r.value < r.lowerBound { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should be > '%f', got '%f'", r.key, r.lowerBound, r.value), - nil, - ) - } - return nil -} - -/* -Float64NotInferiorTo returns a RuleFloat64 for -validating that a float64 is not inferior to -given lower bound. -*/ -func Float64NotInferiorTo(lowerBound float64) RuleFloat64 { - return ruleFloat64NotInferiorTo{ - &baseRuleFloat64{}, - lowerBound, - } -} - -type ruleFloat64NotSuperiorTo struct { - *baseRuleFloat64 - upperBound float64 -} - -func (r ruleFloat64NotSuperiorTo) validate() error { - const op string = "xassert.ruleFloat64NotSuperiorTo.validate" - if r.value > r.upperBound { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should be < '%f', got '%f'", r.key, r.upperBound, r.value), - nil, - ) - } - return nil -} - -/* -Float64NotSuperiorTo returns a RuleFloat64 for -validating that a float64 is not superior to -given upper bound. -*/ -func Float64NotSuperiorTo(upperBound float64) RuleFloat64 { - return ruleFloat64NotSuperiorTo{ - &baseRuleFloat64{}, - upperBound, - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = RuleFloat64(new(ruleFloat64NotInferiorTo)) - _ = RuleFloat64(new(ruleFloat64NotSuperiorTo)) -) diff --git a/internal/pkg/xassert/float64_test.go b/internal/pkg/xassert/float64_test.go deleted file mode 100644 index 78aba945..00000000 --- a/internal/pkg/xassert/float64_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package xassert - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestFloat64NotInferiorTo(t *testing.T) { - rule := Float64NotInferiorTo(0.0) - // should be OK. - rule.with("FOO", 10.0) - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", -10.0) - err = rule.validate() - test.AssertError(t, err) -} - -func TestFloat64NotSuperiorTo(t *testing.T) { - rule := Float64NotSuperiorTo(0.0) - // should be OK. - rule.with("FOO", -10.0) - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", 10.0) - err = rule.validate() - test.AssertError(t, err) -} diff --git a/internal/pkg/xassert/int64.go b/internal/pkg/xassert/int64.go deleted file mode 100644 index c3fc3ae7..00000000 --- a/internal/pkg/xassert/int64.go +++ /dev/null @@ -1,88 +0,0 @@ -package xassert - -import ( - "fmt" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// RuleInt64 is an interface for -// validating an int64. -type RuleInt64 interface { - with(key string, value int64) - validate() error -} - -type baseRuleInt64 struct { - key string - value int64 -} - -func (r *baseRuleInt64) with(key string, value int64) { - r.key = key - r.value = value -} - -type ruleInt64NotInferiorTo struct { - *baseRuleInt64 - lowerBound int64 -} - -func (r ruleInt64NotInferiorTo) validate() error { - const op string = "xassert.ruleInt64NotInferiorTo.validate" - if r.value < r.lowerBound { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should be > '%d', got '%d'", r.key, r.lowerBound, r.value), - nil, - ) - } - return nil -} - -/* -Int64NotInferiorTo returns a RuleInt64 for -validating that an int64 is not inferior to -given lower bound. -*/ -func Int64NotInferiorTo(lowerBound int64) RuleInt64 { - return &ruleInt64NotInferiorTo{ - &baseRuleInt64{}, - lowerBound, - } -} - -type ruleInt64NotSuperiorTo struct { - *baseRuleInt64 - upperBound int64 -} - -func (r ruleInt64NotSuperiorTo) validate() error { - const op string = "xassert.ruleInt64NotSuperiorTo.validate" - if r.value > r.upperBound { - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should be < '%d', got '%d'", r.key, r.upperBound, r.value), - nil, - ) - } - return nil -} - -/* -Int64NotSuperiorTo returns a RuleInt64 for -validating that an int64 is not superior to -given upper bound. -*/ -func Int64NotSuperiorTo(upperBound int64) RuleInt64 { - return ruleInt64NotSuperiorTo{ - &baseRuleInt64{}, - upperBound, - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = RuleInt64(new(ruleInt64NotInferiorTo)) - _ = RuleInt64(new(ruleInt64NotSuperiorTo)) -) diff --git a/internal/pkg/xassert/int64_test.go b/internal/pkg/xassert/int64_test.go deleted file mode 100644 index 2b5244dd..00000000 --- a/internal/pkg/xassert/int64_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package xassert - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestInt64NotInferiorTo(t *testing.T) { - rule := Int64NotInferiorTo(0) - // should be OK. - rule.with("FOO", 10) - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", -10) - err = rule.validate() - test.AssertError(t, err) -} - -func TestInt64NotSuperiorTo(t *testing.T) { - rule := Int64NotSuperiorTo(0) - // should be OK. - rule.with("FOO", -10) - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", 10) - err = rule.validate() - test.AssertError(t, err) -} diff --git a/internal/pkg/xassert/string.go b/internal/pkg/xassert/string.go deleted file mode 100644 index d59bea8a..00000000 --- a/internal/pkg/xassert/string.go +++ /dev/null @@ -1,121 +0,0 @@ -package xassert - -import ( - "fmt" - "strings" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// RuleString is an interface for -// validating a string. -type RuleString interface { - with(key, value string) - validate() error -} - -type baseRuleString struct { - key string - value string -} - -func (r *baseRuleString) with(key, value string) { - r.key = key - r.value = value -} - -type ruleStringOneOf struct { - *baseRuleString - values []string -} - -func (r ruleStringOneOf) validate() error { - const op string = "xassert.ruleStringOneOf.validate" - for _, v := range r.values { - if r.value == v { - return nil - } - } - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should be one of '%v', got '%s'", r.key, r.values, r.value), - nil, - ) -} - -/* -StringOneOf returns a RuleString for -validating that a string is one of given -values. -*/ -func StringOneOf(values []string) RuleString { - return ruleStringOneOf{ - &baseRuleString{}, - values, - } -} - -type ruleStringStartWith struct { - *baseRuleString - startWith string -} - -func (r ruleStringStartWith) validate() error { - const op string = "xassert.ruleStringStartWith.validate" - if strings.HasPrefix(r.value, r.startWith) { - return nil - } - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should start with '%s', got '%s'", r.key, r.startWith, r.value), - nil, - ) -} - -/* -StringStartWith returns a RuleString for -validating that a string starts with -given string. -*/ -func StringStartWith(startWith string) RuleString { - return ruleStringStartWith{ - &baseRuleString{}, - startWith, - } -} - -type ruleStringEndWith struct { - *baseRuleString - endWith string -} - -func (r ruleStringEndWith) validate() error { - const op string = "xassert.ruleStringEndWith.validate" - if strings.HasSuffix(r.value, r.endWith) { - return nil - } - return xerror.Invalid( - op, - fmt.Sprintf("'%s' should end with '%s', got '%s'", r.key, r.endWith, r.value), - nil, - ) -} - -/* -StringEndWith returns a RuleString for -validating that a string ends with -given string. -*/ -func StringEndWith(endWith string) RuleString { - return ruleStringEndWith{ - &baseRuleString{}, - endWith, - } -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = RuleString(new(ruleStringOneOf)) - _ = RuleString(new(ruleStringStartWith)) - _ = RuleString(new(ruleStringEndWith)) -) diff --git a/internal/pkg/xassert/string_test.go b/internal/pkg/xassert/string_test.go deleted file mode 100644 index fb1c0d4e..00000000 --- a/internal/pkg/xassert/string_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package xassert - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestStringOfOne(t *testing.T) { - rule := StringOneOf([]string{"foo", "bar", "baz"}) - // should be OK. - rule.with("FOO", "foo") - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", "qux") - err = rule.validate() - test.AssertError(t, err) -} - -func TestStringStartWith(t *testing.T) { - rule := StringStartWith("foo") - // should be OK. - rule.with("FOO", "foobarfoo") - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", "qux") - err = rule.validate() - test.AssertError(t, err) -} - -func TestStringEndWith(t *testing.T) { - rule := StringEndWith("foo") - // should be OK. - rule.with("FOO", "foobarfoo") - err := rule.validate() - assert.Nil(t, err) - // should not be OK. - rule.with("FOO", "qux") - err = rule.validate() - test.AssertError(t, err) -} diff --git a/internal/pkg/xassert/xassert.go b/internal/pkg/xassert/xassert.go deleted file mode 100644 index 8ad5a55a..00000000 --- a/internal/pkg/xassert/xassert.go +++ /dev/null @@ -1,234 +0,0 @@ -package xassert - -import ( - "fmt" - "os" - "strconv" - - "github.com/dustin/go-humanize" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -/* -String applies validation on a string. - -If string is empty or validation fails, -returns the default value. - -The key is used to identify the value. -*/ -func String(key, value, defaultValue string, rules ...RuleString) (string, error) { - const op string = "xassert.String" - result := defaultValue - if value != "" { - result = value - } - for _, rule := range rules { - rule.with(key, result) - if err := rule.validate(); err != nil { - return defaultValue, xerror.New(op, err) - } - } - return result, nil -} - -/* -StringFromEnv returns the value of given environment -variable or the default value if not found or -validation fails. -*/ -func StringFromEnv(envVar, defaultValue string, rules ...RuleString) (string, error) { - const op string = "xassert.StringFromEnv" - value := os.Getenv(envVar) - result, err := String(envVar, value, defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Int64 tries to convert a string to an int64. - -If string is empty, conversion or validation fails, -returns the default value. - -The key is used to identify the value. -*/ -func Int64(key, value string, defaultValue int64, rules ...RuleInt64) (int64, error) { - const op string = "xassert.Int64" - result := defaultValue - if value != "" { - parsedValue, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return defaultValue, xerror.Invalid( - op, - fmt.Sprintf("'%s' is not an integer, got '%s'", key, value), - err, - ) - } - result = parsedValue - } - for _, rule := range rules { - rule.with(key, result) - if err := rule.validate(); err != nil { - return defaultValue, xerror.New(op, err) - } - } - return result, nil -} - -/* -Int64FromEnv returns the int64 representation of the -value of given environment variable. - -If not found, empty, conversion or validation fails, -returns the default value. -*/ -func Int64FromEnv(envVar string, defaultValue int64, rules ...RuleInt64) (int64, error) { - const op string = "xassert.Int64FromEnv" - value := os.Getenv(envVar) - result, err := Int64(envVar, value, defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Float64 tries to convert a string to a float64. - -If string is empty, conversion or validation fails, -returns the default value. - -The key is used to identify the value. -*/ -func Float64(key, value string, defaultValue float64, rules ...RuleFloat64) (float64, error) { - const op string = "xassert.Float64" - result := defaultValue - if value != "" { - parsedValue, err := strconv.ParseFloat(value, 64) - if err != nil { - return defaultValue, xerror.Invalid( - op, - fmt.Sprintf("'%s' is not a float, got '%s'", key, value), - err, - ) - } - result = parsedValue - } - for _, rule := range rules { - rule.with(key, result) - if err := rule.validate(); err != nil { - return defaultValue, xerror.New(op, err) - } - } - return result, nil -} - -/* -Float64FromEnv returns the float64 representation of the -value of given environment variable. - -If not found, empty, conversion or validation fails, -returns the default value. -*/ -func Float64FromEnv(envVar string, defaultValue float64, rules ...RuleFloat64) (float64, error) { - const op string = "xassert.Float64FromEnv" - value := os.Getenv(envVar) - result, err := Float64(envVar, value, defaultValue, rules...) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Bool tries to convert a string to a boolean. - -If string is empty or conversion fails, returns the -default value. - -The key is used to identify the value. -*/ -func Bool(key, value string, defaultValue bool) (bool, error) { - const op string = "xassert.Bool" - result := defaultValue - if value != "" { - parsedValue, err := strconv.ParseBool(value) - if err != nil { - return defaultValue, xerror.Invalid( - op, - fmt.Sprintf("'%s' is not a boolean, got '%s'", key, value), - err, - ) - } - result = parsedValue - } - return result, nil -} - -/* -BoolFromEnv returns the boolean representation of the -value of given environment variable. - -If not found, empty or conversion fails, returns the -default value. -*/ -func BoolFromEnv(envVar string, defaultValue bool) (bool, error) { - const op string = "xassert.BoolFromEnv" - value := os.Getenv(envVar) - result, err := Bool(envVar, value, defaultValue) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} - -/* -Bytes tries to convert a string to a int64. - -If string is empty or conversion fails, returns the -default value. - -The key is used to identify the value. -*/ -func Bytes(key, value string, defaultValue int64, rules ...RuleInt64) (int64, error) { - const op string = "xassert.Bytes" - result := defaultValue - if value != "" { - parsedValue, err := humanize.ParseBigBytes(value) - if err != nil { - return defaultValue, xerror.Invalid( - op, - fmt.Sprintf("'%s' is not a correct bytes representation, got '%s'", key, value), - err, - ) - } - result = parsedValue.Int64() - } - for _, rule := range rules { - rule.with(key, result) - if err := rule.validate(); err != nil { - return defaultValue, xerror.New(op, err) - } - } - return result, nil -} - -/* -BytesFromEnv returns the int64 representation of the -value of given environment variable. - -If not found, empty or conversion fails, returns the -default value. -*/ -func BytesFromEnv(envVar string, defaultValue int64, rules ...RuleInt64) (int64, error) { - const op string = "xassert.BytesFromEnv" - value := os.Getenv(envVar) - result, err := Bytes(envVar, value, defaultValue) - if err != nil { - return result, xerror.New(op, err) - } - return result, nil -} diff --git a/internal/pkg/xassert/xassert_test.go b/internal/pkg/xassert/xassert_test.go deleted file mode 100644 index 32a5e2ac..00000000 --- a/internal/pkg/xassert/xassert_test.go +++ /dev/null @@ -1,289 +0,0 @@ -package xassert - -import ( - "os" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestString(t *testing.T) { - const ( - defaultValue string = "FOO" - ) - var expected string - rule := StringOneOf([]string{"FOO", "BAR"}) - // empty value, result should be equal - // to the default value. - v, err := String("foo", "", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // as it is one of "FOO" and "BAR". - expected = "FOO" - v, err = String("foo", expected, defaultValue, rule) - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // one of "FOO" and "BAR". - v, err = String("foo", "BAZ", defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) -} - -func TestStringFromEnv(t *testing.T) { - const ( - envVar string = "FOO" - defaultValue string = "FOO" - ) - var expected string - rule := StringOneOf([]string{"FOO", "BAR"}) - // no environment variable set, - // value should be equal to default value. - v, err := StringFromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to environment variable - // value as it is one of "FOO" and "BAR". - expected = "BAR" - os.Setenv(envVar, expected) - v, err = StringFromEnv(envVar, defaultValue, rule) - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value is not one of "FOO" and "BAR". - os.Setenv(envVar, "BAZ") - v, err = StringFromEnv(envVar, defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) -} - -func TestInt64(t *testing.T) { - const ( - defaultValue int64 = 10 - ) - var expected int64 - rule := Int64NotInferiorTo(6) - // empty value, result should be equal - // to the default value. - v, err := Int64("foo", "", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as integer. - v, err = Int64("foo", "5", defaultValue) - expected = 5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of an integer. - v, err = Int64("foo", "foo", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // should not be OK as given value does not - // validate the rule x >= 6. - v, err = Int64("foo", "5", defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) -} - -func TestInt64FromEnv(t *testing.T) { - const ( - envVar string = "FOO" - defaultValue int64 = 10 - ) - var expected int64 - rule := Int64NotInferiorTo(6) - // no environment variable set, - // value should be equal to default value. - v, err := Int64FromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to environment variable - // value but as integer. - os.Setenv(envVar, "5") - v, err = Int64FromEnv(envVar, defaultValue) - expected = 5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value is not a string representation of an integer. - os.Setenv(envVar, "foo") - v, err = Int64FromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value does not validate the rule x >= 6. - os.Setenv(envVar, "5") - v, err = Int64FromEnv(envVar, defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) -} - -func TestFloat64(t *testing.T) { - const defaultValue float64 = 10.0 - var expected float64 - rule := Float64NotInferiorTo(6.0) - // empty value, result should be equal - // to the default value. - v, err := Float64("foo", "", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as float. - v, err = Float64("foo", "5.5", defaultValue) - expected = 5.5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of a float. - v, err = Float64("foo", "foo", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - // should not be OK as given value does not - // validate the rule x >= 6. - v, err = Float64("foo", "5.0", defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) -} - -func TestFloat64FromEnv(t *testing.T) { - const ( - envVar string = "FOO" - defaultValue float64 = 10.0 - ) - var expected float64 - rule := Float64NotInferiorTo(6.0) - // no environment variable set, - // value should be equal to default value. - v, err := Float64FromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to environment variable - // value but as float. - os.Setenv(envVar, "5.5") - v, err = Float64FromEnv(envVar, defaultValue) - expected = 5.5 - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value is not a string representation of a float. - os.Setenv(envVar, "foo") - v, err = Float64FromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value does not validate the rule x >= 6. - os.Setenv(envVar, "5.0") - v, err = Float64FromEnv(envVar, defaultValue, rule) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) -} - -func TestBool(t *testing.T) { - const defaultValue bool = true - var expected bool - // empty value, result should be equal - // to the default value. - v, err := Bool("foo", "", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to given value - // but as boolean. - v, err = Bool("foo", "1", defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - v, err = Bool("foo", "true", defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - v, err = Bool("foo", "0", defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - v, err = Bool("foo", "false", defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - // should not be OK as given value is not - // a string representation of a boolean. - v, err = Bool("foo", "foo", defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) -} - -func TestBoolFromEnv(t *testing.T) { - const ( - envVar string = "FOO" - defaultValue bool = true - ) - var expected bool - // no environment variable set, - // value should be equal to default value. - v, err := BoolFromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - assert.Nil(t, err) - // result should be equal to environment variable - // value but as boolean. - os.Setenv(envVar, "1") - v, err = BoolFromEnv(envVar, defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - os.Setenv(envVar, "true") - v, err = BoolFromEnv(envVar, defaultValue) - expected = true - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - os.Setenv(envVar, "0") - v, err = BoolFromEnv(envVar, defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - os.Setenv(envVar, "false") - v, err = BoolFromEnv(envVar, defaultValue) - expected = false - assert.Equal(t, expected, v) - assert.Nil(t, err) - os.Unsetenv(envVar) - // should not be OK as environment variable - // value is not a string representation of a boolean. - os.Setenv(envVar, "foo") - v, err = BoolFromEnv(envVar, defaultValue) - expected = defaultValue - assert.Equal(t, expected, v) - test.AssertError(t, err) - os.Unsetenv(envVar) -} diff --git a/internal/pkg/xcontext/doc.go b/internal/pkg/xcontext/doc.go deleted file mode 100644 index d6a369d6..00000000 --- a/internal/pkg/xcontext/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package xcontext helps managing -// context.Context with timeout. -package xcontext diff --git a/internal/pkg/xcontext/xcontext.go b/internal/pkg/xcontext/xcontext.go deleted file mode 100644 index 2c4259e4..00000000 --- a/internal/pkg/xcontext/xcontext.go +++ /dev/null @@ -1,56 +0,0 @@ -package xcontext - -import ( - "context" - "fmt" - "strings" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" -) - -// WithTimeout creates a context.Context which -// times out after given seconds. -func WithTimeout(logger xlog.Logger, seconds float64) (context.Context, context.CancelFunc) { - const op string = "xcontext.WithTimeout" - logger.DebugOpf(op, "creating context with '%.2fs' of timeout...", seconds) - return context.WithTimeout(context.Background(), xtime.Duration(seconds)) -} - -/* -MustHandleError checks if there is an error -in the given Context. - -If no error, returns the previous error. - -If context.DeadlineExceeded, wraps the previous -error inside an xerror.Error with xerror.TimeoutCode. - -Otherwise wraps the previous error inside an -xerror.Error. - -It panics if no previous error. -*/ -func MustHandleError(ctx context.Context, previousErr error) error { - const op string = "xcontext.MustHandleError" - if previousErr == nil { - panic(fmt.Sprintf("%s: previous error should not be nil", op)) - } - err := ctx.Err() - if err == nil { - // we do not wrap the previous error - // as it should be wrapped by the caller. - return previousErr - } - // context has timed out - if strings.Contains(err.Error(), context.DeadlineExceeded.Error()) { - return xerror.Timeout(op, "context has timed out", previousErr) - } - /* - context has another error: we do not - wrap the error from the Context as the previous - error should contain it. - */ - return xerror.New(op, previousErr) -} diff --git a/internal/pkg/xcontext/xcontext_test.go b/internal/pkg/xcontext/xcontext_test.go deleted file mode 100644 index 36a7e9d9..00000000 --- a/internal/pkg/xcontext/xcontext_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package xcontext - -import ( - "errors" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xtime" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestMustHandleError(t *testing.T) { - previousErr := errors.New("previous error") - logger := test.DebugLogger() - // context should not have an error. - ctx, cancel := WithTimeout(logger, 5) - defer cancel() - err := MustHandleError(ctx, previousErr) - assert.Equal(t, previousErr, err) - // should panic. - ctx, cancel = WithTimeout(logger, 5) - defer cancel() - assert.Panics(t, func() { - MustHandleError(ctx, nil) - }) - // context should timed out. - ctx, cancel = WithTimeout(logger, 0.5) - defer cancel() - time.Sleep(xtime.Duration(1)) - err = MustHandleError(ctx, previousErr) - xerr := test.AssertError(t, err) - assert.Equal(t, xerror.TimeoutCode, xerror.Code(xerr)) - // context should have an error different - // than context.DeadlineExceeded. - ctx, cancel = WithTimeout(logger, 5) - cancel() - err = MustHandleError(ctx, previousErr) - xerr = test.AssertError(t, err) - assert.Equal(t, xerror.InternalCode, xerror.Code(xerr)) -} diff --git a/internal/pkg/xerror/doc.go b/internal/pkg/xerror/doc.go deleted file mode 100644 index 999decf8..00000000 --- a/internal/pkg/xerror/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -/* -Package xerror helps standardizing -the errors through the application. - -Credits: https://middlemost.com/failure-is-your-domain/ -*/ -package xerror diff --git a/internal/pkg/xerror/xerror.go b/internal/pkg/xerror/xerror.go deleted file mode 100644 index f1f9c1ee..00000000 --- a/internal/pkg/xerror/xerror.go +++ /dev/null @@ -1,152 +0,0 @@ -package xerror - -import ( - "bytes" - "fmt" - "strings" -) - -// ErrorCode is machine-readable error code. -type ErrorCode string - -const ( - // InternalCode is an internal error. - InternalCode ErrorCode = "internal" - // InvalidCode occurs when a validation - // failed. - InvalidCode ErrorCode = "invalid" - // TimeoutCode occurs when something - // timed out. - TimeoutCode ErrorCode = "timeout" -) - -// Error defines our standard application -// error. -type Error struct { - code ErrorCode - message string - op string - err error -} - -// Error returns the string representation of the error message. -func (e Error) Error() string { - var buf bytes.Buffer - // if wrapping an error, print its Error() message. - // Otherwise print the error code & message. - if e.err != nil { - buf.WriteString(e.err.Error()) - } else { - if e.code != "" { - fmt.Fprintf(&buf, "<%s> ", e.code) - } - buf.WriteString(e.message) - } - return buf.String() -} - -/* -New returns a xerror.Error. - -Should be used for wrapping an error -at the end of a function. -*/ -func New(op string, previous error) error { - return &Error{ - op: op, - err: previous, - } -} - -/* -Invalid returns a xerror.Error. - -Should be used when an input -is wrong. -*/ -func Invalid(op, message string, previous error) error { - return &Error{ - code: InvalidCode, - message: message, - op: op, - err: previous, - } -} - -/* -Timeout returns a xerror.Error. - -Should be used when a timeout occurs. -*/ -func Timeout(op, message string, previous error) error { - return &Error{ - code: TimeoutCode, - message: message, - op: op, - err: previous, - } -} - -// Code returns the code of the root error, if available. -// Otherwise returns InternalCode. -func Code(err error) ErrorCode { - if err == nil { - return "" - } - e, ok := err.(*Error) - if ok && e.code != "" { - return e.code - } - if ok && e.err != nil { - return Code(e.err) - } - return InternalCode -} - -const defaultMessage string = "an internal error has occurred: please contact technical support" - -// Message returns the human-readable message of the error, if available. -// Otherwise returns a generic error message. -func Message(err error) string { - if err == nil { - return "" - } - e, ok := err.(*Error) - if ok && e.message != "" { - return e.message - } - if ok && e.err != nil { - return Message(e.err) - } - return defaultMessage -} - -// Op returns the logical operation of the error, if available. -// Otherwise returns an empty string. -func Op(err error) string { - if err == nil { - return "" - } - e, ok := err.(*Error) - if !ok { - return "" - } - var buf bytes.Buffer - nestedOp := Op(e.err) - if nestedOp != "" { - // we want to avoid having the same op chained. - if e.op != "" && !strings.Contains(nestedOp, e.op) { - fmt.Fprintf(&buf, "%s: %s", e.op, nestedOp) - } else { - fmt.Fprintf(&buf, "%s", nestedOp) - } - } else if e.op != "" { - fmt.Fprintf(&buf, "%s", e.op) - } - return buf.String() -} - -// Compile-time checks to ensure type implements desired interfaces. -var ( - _ = error(new(Error)) -) diff --git a/internal/pkg/xerror/xerror_test.go b/internal/pkg/xerror/xerror_test.go deleted file mode 100644 index a3727ccf..00000000 --- a/internal/pkg/xerror/xerror_test.go +++ /dev/null @@ -1,97 +0,0 @@ -package xerror - -import ( - "errors" - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -/* -Error 1.0: op = "foo" -Error 1.1: op = "bar" -Error 1.2: code = "invalid", op = "baz", message = "nested error" -Error 1.3: message = "root error" -*/ -func scenario1() error { - rootErr := errors.New("root error") - nestedErr := Invalid("baz", "nested error", rootErr) - wrappingErr := New("bar", nestedErr) - return New("foo", wrappingErr) -} - -/* -Error 2.0: op = "foo" -Error 2.1: op = "bar" -Error 2.2: code = "timeout", op = "bar", message = "nested error" -*/ -func scenario2() error { - nestedErr := Timeout("bar", "nested error", nil) - wrappingErr := New("bar", nestedErr) - return New("foo", wrappingErr) -} - -// Error 3.0: code = "", op = "foo" -func scenario3() error { - return New("foo", nil) -} - -func TestError(t *testing.T) { - // should return the Error 1.3 - // message. - err := scenario1() - assert.Equal(t, "root error", err.Error()) - // should return the Error 2.2 message with - // its code. - err = scenario2() - assert.Equal(t, " nested error", err.Error()) -} - -func TestCode(t *testing.T) { - // should be an empty code if no error. - assert.Equal(t, "", fmt.Sprintf("%s", Code(nil))) - // should be the code of Error 1.2. - err := scenario1() - assert.Equal(t, InvalidCode, Code(err)) - // should be the code of Error 2.2. - err = scenario2() - assert.Equal(t, TimeoutCode, Code(err)) - // should be the default code. - err = scenario3() - assert.Equal(t, InternalCode, Code(err)) - err = errors.New("some error") - assert.Equal(t, InternalCode, Code(err)) -} - -func TestMessage(t *testing.T) { - // should be an empty message if no error. - assert.Equal(t, "", Message(nil)) - // should be the message of Error 1.2. - err := scenario1() - assert.Equal(t, "nested error", Message(err)) - // should be the default message. - err = errors.New("some error") - assert.Equal(t, defaultMessage, Message(err)) -} - -func TestOp(t *testing.T) { - // should be an empty op if no error. - assert.Equal(t, "", Op(nil)) - // should be the chain of op in this order: - // Error 1.0 -> Error 1.1 -> Error 1.2. - err := scenario1() - assert.Equal(t, "foo: bar: baz", Op(err)) - /* - should be the chain of op in this order: - Error 2.0 -> Error 2.1. - - As Error 2.1 and Error 2.2 shares the same - op, Error 2.2 op is not displayed. - */ - err = scenario2() - assert.Equal(t, "foo: bar", Op(err)) - // should be an empty op if not Error. - err = errors.New("some error") - assert.Equal(t, "", Op(err)) -} diff --git a/internal/pkg/xexec/doc.go b/internal/pkg/xexec/doc.go deleted file mode 100644 index 9db49c6f..00000000 --- a/internal/pkg/xexec/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -/* -Package xexec helps creating exec.Cmd -with logging and executing those commands -without leaking orphan processes. - -All functions return our standard xerror.Error -in case of error. -*/ -package xexec diff --git a/internal/pkg/xexec/xexec.go b/internal/pkg/xexec/xexec.go deleted file mode 100644 index 31dc7fe7..00000000 --- a/internal/pkg/xexec/xexec.go +++ /dev/null @@ -1,155 +0,0 @@ -package xexec - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "os/exec" - "strings" - "syscall" - - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -/* -Command is a wrapper around exec.Command. - -If given xlog.Logger has a xlog.DebugLevel, -also logs the output from the command. -*/ -func Command(logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) { - const op string = "xexec.Command" - cmd := exec.Command(binary, args...) - if err := pipe(logger, cmd); err != nil { - return nil, xerror.New(op, err) - } - return cmd, nil -} - -/* -CommandContext is a wrapper around exec.CommandContext. - -If given xlog.Logger has a xlog.DebugLevel, -also logs the output from the command. -*/ -func CommandContext(ctx context.Context, logger xlog.Logger, binary string, args ...string) (*exec.Cmd, error) { - const op string = "xexec.CommandContext" - cmd := exec.CommandContext(ctx, binary, args...) - if err := pipe(logger, cmd); err != nil { - return nil, xerror.New(op, err) - } - return cmd, nil -} - -/* -Run runs a command. - -If command finishes or fails to finish -before context.Context deadline, kill the -corresponding process in a way which does -not leak orphan processes. -*/ -func Run(ctx context.Context, logger xlog.Logger, binary string, args ...string) error { - const op string = "xexec.Run" - resolver := func() error { - cmd, err := Command( - logger, - binary, - args..., - ) - if err != nil { - return err - } - LogBeforeExecute(logger, cmd) - // see https://medium.com/@felixge/killing-a-child-process-and-all-of-its-children-in-go-54079af94773. - kill := func() { - err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) - if err == nil { - return - } - if !strings.Contains(err.Error(), "no such process") { - logger.ErrorOp(op, err) - } - } - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - if err := cmd.Start(); err != nil { - return err - } - result := make(chan error, 1) - go func() { - result <- cmd.Wait() - }() - select { - case err := <-result: - logger.DebugOpf(op, "command '%s' finished", strings.Join(cmd.Args, " ")) - kill() - return err - case <-ctx.Done(): - logger.DebugOpf(op, "command '%s' failed to finish before context.Context deadline", strings.Join(cmd.Args, " ")) - kill() - return ctx.Err() - } - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -// LogBeforeExecute logs a command before its execution. -func LogBeforeExecute(logger xlog.Logger, cmd *exec.Cmd) { - const op string = "xexec.LogBeforeExecute" - logger.DebugOpf(op, "executing command: %s", strings.Join(cmd.Args, " ")) -} - -func pipe(logger xlog.Logger, cmd *exec.Cmd) error { - const op string = "xexec.pipe" - if logger.Level() != xlog.DebugLevel { - return nil - } - // if xlog.DebugLevel, log the output - // from the command. - resolver := func() error { - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - go logCommandOutput(logger, stdout, "stdout", cmd) - go logCommandOutput(logger, stderr, "stderr", cmd) - return nil - } - if err := resolver(); err != nil { - return xerror.New(op, err) - } - return nil -} - -func logCommandOutput(logger xlog.Logger, reader io.ReadCloser, outputType string, cmd *exec.Cmd) { - var buf bytes.Buffer - buf.WriteString(outputType) - for _, arg := range cmd.Args { - buf.WriteString(fmt.Sprintf(".%s", arg)) - } - op := buf.String() - r := bufio.NewReader(reader) - defer reader.Close() - for { - line, _, err := r.ReadLine() - if err != nil { - if err != io.EOF && !strings.Contains(err.Error(), "file already closed") { - logger.ErrorOp(op, err) - } - break - } - if len(line) != 0 { - logger.DebugOp(op, string(line)) - } - } -} diff --git a/internal/pkg/xexec/xexec_test.go b/internal/pkg/xexec/xexec_test.go deleted file mode 100644 index ceffc5c5..00000000 --- a/internal/pkg/xexec/xexec_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package xexec - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xcontext" - "github.com/thecodingmachine/gotenberg/test" -) - -func TestCommand(t *testing.T) { - logger := test.DebugLogger() - // should pipe command output as - // xlog.Logger has a xlog.DebugLevel. - cmd, err := Command(logger, "echo", "Hello", "World") - assert.Nil(t, err) - LogBeforeExecute(logger, cmd) - // should not pipe command output as - // xlog.Logger has a xlog.InfoLevel. - logger = test.InfoLogger() - cmd, err = Command(logger, "echo", "Hello", "World") - LogBeforeExecute(logger, cmd) - assert.Nil(t, err) -} - -func TestCommandContext(t *testing.T) { - logger := test.DebugLogger() - // should pipe command output as - // xlog.Logger has a xlog.DebugLevel. - cmd, err := CommandContext(context.Background(), logger, "echo", "Hello", "World") - assert.Nil(t, err) - LogBeforeExecute(logger, cmd) - // should not pipe command output as - // xlog.Logger has a xlog.InfoLevel. - logger = test.InfoLogger() - cmd, err = CommandContext(context.Background(), logger, "echo", "Hello", "World") - LogBeforeExecute(logger, cmd) - assert.Nil(t, err) -} - -func TestRun(t *testing.T) { - logger := test.DebugLogger() - // should run without issue. - err := Run(context.Background(), logger, "echo", "Hello", "World") - assert.Nil(t, err) - // should not be OK as context.Context - // should timeout. - ctx, cancel := xcontext.WithTimeout(logger, 0) - defer cancel() - err = Run(ctx, logger, "echo", "Hello", "World") - assert.NotNil(t, err) -} diff --git a/internal/pkg/xlog/doc.go b/internal/pkg/xlog/doc.go deleted file mode 100644 index 7aab9e5c..00000000 --- a/internal/pkg/xlog/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Package xlog defines a standard logger -for the application. - -It uses structured logging thanks to -https://github.com/sirupsen/logrus. - -All messages have at least two fields: - -A "trace" field which helps to identify -messages belonging to the same context. - -An "op" field which helps to identify -the logical operation associated -with the message. -*/ -package xlog diff --git a/internal/pkg/xlog/xlog.go b/internal/pkg/xlog/xlog.go deleted file mode 100644 index af571d91..00000000 --- a/internal/pkg/xlog/xlog.go +++ /dev/null @@ -1,141 +0,0 @@ -package xlog - -import ( - "fmt" - "os" - - "github.com/mattn/go-isatty" - "github.com/sirupsen/logrus" -) - -// Level helps setting the severity -// of the messages displayed. -type Level string - -const ( - // DebugLevel is the lowest level. - DebugLevel Level = "DEBUG" - // InfoLevel is the intermediate level. - InfoLevel Level = "INFO" - // ErrorLevel is the highest level. - ErrorLevel Level = "ERROR" -) - -// Logger enforces specific log message formats. -type Logger struct { - entry *logrus.Entry - level Level -} - -// New returns a xlog.Logger. -func New(level Level, trace string) Logger { - l := logrus.New() - l.SetLevel(mustLogrusLevel(level)) - if !isatty.IsTerminal(os.Stdout.Fd()) { - l.SetFormatter(&logrus.JSONFormatter{}) - } - return Logger{ - entry: l.WithField("trace", trace), - level: level, - } -} - -func mustLogrusLevel(level Level) logrus.Level { - const op string = "xlog.mustLogrusLevel" - switch level { - case DebugLevel: - return logrus.DebugLevel - case InfoLevel: - return logrus.InfoLevel - case ErrorLevel: - return logrus.ErrorLevel - default: - panic(fmt.Sprintf("%s: '%s' is not associated with any logrus.Level", op, level)) - } -} - -// Levels returns a slice of string -// with all severities. -func Levels() []string { - return []string{ - string(DebugLevel), - string(InfoLevel), - string(ErrorLevel), - } -} - -/* -MustParseLevel returns the Level corresponding -to given string. - -It panics if no correspondence. -*/ -func MustParseLevel(level string) Level { - const op string = "xlog.MustParseLevel" - switch level { - case string(DebugLevel): - return DebugLevel - case string(InfoLevel): - return InfoLevel - case string(ErrorLevel): - return ErrorLevel - default: - panic(fmt.Sprintf("%s: '%s' is not one of '%v'", op, level, Levels())) - } -} - -// Level returns the current Level. -func (l Logger) Level() Level { - return l.level -} - -// WithFields returns a new xlog.Logger with -// given fields. -func (l Logger) WithFields(fields map[string]interface{}) Logger { - return Logger{ - entry: l.entry.WithFields(fields), - level: l.level, - } -} - -// DebugOp logs a debug message for given -// logical operation. -func (l Logger) DebugOp(op, message string) { - l.entry.WithField("op", op).Debug(message) -} - -// DebugOpf logs a debug message for given -// logical operation and format. -func (l Logger) DebugOpf(op, format string, args ...interface{}) { - l.entry.WithField("op", op).Debugf(format, args...) -} - -// InfoOp logs an info message for given -// logical operation. -func (l Logger) InfoOp(op, message string) { - l.entry.WithField("op", op).Info(message) -} - -// InfoOpf logs an info message for given -// logical operation and format. -func (l Logger) InfoOpf(op, format string, args ...interface{}) { - l.entry.WithField("op", op).Infof(format, args...) -} - -// ErrorOp logs an error for given -// logical operation. -func (l Logger) ErrorOp(op string, err error) { - l.entry.WithField("op", op).Error(err.Error()) -} - -// ErrorOpf logs an error message for given -// logical operation and format. -func (l Logger) ErrorOpf(op, format string, args ...interface{}) { - l.entry.WithField("op", op).Errorf(format, args...) -} - -// FatalOp logs an error for given -// logical operation and exit 1. -func (l Logger) FatalOp(op string, err error) { - l.entry.WithField("op", op).Fatal(err.Error()) -} diff --git a/internal/pkg/xrand/doc.go b/internal/pkg/xrand/doc.go deleted file mode 100644 index 672af2b3..00000000 --- a/internal/pkg/xrand/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package xrand helps generating -// random strings. -package xrand diff --git a/internal/pkg/xrand/xrand.go b/internal/pkg/xrand/xrand.go deleted file mode 100644 index bdac8fab..00000000 --- a/internal/pkg/xrand/xrand.go +++ /dev/null @@ -1,10 +0,0 @@ -package xrand - -import ( - "github.com/labstack/gommon/random" -) - -// Get returns a random string. -func Get() string { - return random.String(32) -} diff --git a/internal/pkg/xrand/xrand_test.go b/internal/pkg/xrand/xrand_test.go deleted file mode 100644 index e5880f99..00000000 --- a/internal/pkg/xrand/xrand_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package xrand - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGet(t *testing.T) { - var rands []string - // use case: for 1 000 concurrent - // requests (which is a big Gotenberg instance), - // none should have the same identifier. - for i := 0; i < 1000; i++ { - rands = append(rands, Get()) - } - unique := func() bool { - for i, rand := range rands { - for j, current := range rands { - if i != j && rand == current { - return false - } - } - } - return true - } - assert.Equal(t, true, unique()) -} diff --git a/internal/pkg/xtime/doc.go b/internal/pkg/xtime/doc.go deleted file mode 100644 index 2dbfa55a..00000000 --- a/internal/pkg/xtime/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -/* -Package xtime helps generating -time.Duration from seconds represented -as float64. -*/ -package xtime diff --git a/internal/pkg/xtime/xtime.go b/internal/pkg/xtime/xtime.go deleted file mode 100644 index c0af3bdb..00000000 --- a/internal/pkg/xtime/xtime.go +++ /dev/null @@ -1,10 +0,0 @@ -package xtime - -import ( - "time" -) - -// Duration creates a time.Duration from seconds. -func Duration(seconds float64) time.Duration { - return time.Duration(1000*seconds) * time.Millisecond -} diff --git a/internal/pkg/xtime/xtime_test.go b/internal/pkg/xtime/xtime_test.go deleted file mode 100644 index 67fee437..00000000 --- a/internal/pkg/xtime/xtime_test.go +++ /dev/null @@ -1,14 +0,0 @@ -package xtime - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func TestDuration(t *testing.T) { - expected := time.Duration(1500) * time.Millisecond - result := Duration(1.5) - assert.Equal(t, expected.String(), result.String()) -} diff --git a/loadtesting/README.md b/loadtesting/README.md deleted file mode 100644 index eb673c9f..00000000 --- a/loadtesting/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Load testing - -You may wonder how Gotenberg behaves under load. - -In order to help you having an idea, we created a bunch of scenarios for the -[k6](https://docs.k6.io/docs) load testing tool. - -The Gotenberg container (version `6.0.0` and default options) was hosted on a AWS EC2 `t2.micro` instance (1 vCPU and 1 Go of RAM, low to average network performances). - -The k6 scenarios have been performed on a MacBook Pro 2016 (2 GHz Intel Core i5 and 16 Go 1867 MHz LPDDR3). - -## HTML - -The HTML scenario is quite simple: - -* Ramp up from 0 to 100 virtual users, each one uploading as many time as possible the [HTML test data](../test/testdata/html). -* Stop when at least one response is not an HTTP 200 code. - -```bash -$ k6 run --env MAX_VUS=100 --env BASE_URL=http://ec2-foo.eu-west-1.compute.amazonaws.com html.js - - /\ |‾‾| /‾‾/ /‾/ - /\ / \ | |_/ / / / - / \/ \ | | / ‾‾\ - / \ | |‾\ \ | (_) | - / __________ \ |__| \__\ \___/ .io - - execution: local - output: - - script: html.js - - duration: -, iterations: - - vus: 1, max: 100 - - done [==========================================================] 1m17.9s / 10m0s - - ✗ is status 200 - ↳ 99% — ✓ 201 / ✗ 2 - ✗ is not status 504 - ↳ 99% — ✓ 201 / ✗ 2 - ✓ is not status 500 - - checks.....................: 99.34% ✓ 605 ✗ 4 - data_received..............: 46 MB 594 kB/s - data_sent..................: 9.6 MB 123 kB/s - ✗ failed requests............: 2 0.025646/s - http_req_blocked...........: avg=1.63ms min=2µs med=4µs max=74.77ms p(90)=9.6µs p(95)=19.66ms - http_req_connecting........: avg=1.61ms min=0s med=0s max=74.59ms p(90)=0s p(95)=19.57ms - http_req_duration..........: avg=2.43s min=1.31s med=1.67s max=10.27s p(90)=5.44s p(95)=8.75s - http_req_receiving.........: avg=97.95ms min=103µs med=93.74ms max=202.48ms p(90)=114.51ms p(95)=132.99ms - http_req_sending...........: avg=231.53µs min=92µs med=201µs max=2.33ms p(90)=310.4µs p(95)=333.59µs - http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s - http_req_waiting...........: avg=2.33s min=1.22s med=1.58s max=10.12s p(90)=5.33s p(95)=8.62s - http_reqs..................: 203 2.603019/s - iteration_duration.........: avg=2.43s min=1.31s med=1.68s max=10.27s p(90)=5.45s p(95)=8.75s - iterations.................: 203 2.603019/s - vus........................: 13 min=1 max=13 - vus_max....................: 100 min=100 max=100 -``` - -In our use case, when reaching 13 virtual users (~2,6 requests per second), some incoming requests cannot be fulfilled before 10 seconds (`DEFAULT_WAIT_TIMEOUT` value). -During this test, CPU usage was high and memory usage went from 339 MiB to a peak of 421 MiB before going back to 364 MiB. - -## Office - -The Office scenario is the same as the HTML scenario, but with a [document.docx](../test/testdata/office/document.docx). - -```bash -$ k6 run --env MAX_VUS=100 --env BASE_URL=http://ec2-foo.eu-west-1.compute.amazonaws.com office.js - - /\ |‾‾| /‾‾/ /‾/ - /\ / \ | |_/ / / / - / \/ \ | | / ‾‾\ - / \ | |‾\ \ | (_) | - / __________ \ |__| \__\ \___/ .io - - execution: local - output: - - script: office.js - - duration: -, iterations: - - vus: 1, max: 100 - - done [==========================================================] 47.9s / 10m0s - - ✓ is not status 500 - ✗ is status 200 - ↳ 83% — ✓ 31 / ✗ 6 - ✗ is not status 504 - ↳ 83% — ✓ 31 / ✗ 6 - - checks.....................: 89.18% ✓ 99 ✗ 12 - data_received..............: 2.6 MB 54 kB/s - data_sent..................: 3.4 MB 71 kB/s - ✗ failed requests............: 6 0.125047/s - http_req_blocked...........: avg=4.39ms min=3µs med=4µs max=24.52ms p(90)=23.27ms p(95)=23.62ms - http_req_connecting........: avg=4.34ms min=0s med=0s max=24.42ms p(90)=23.15ms p(95)=23.52ms - http_req_duration..........: avg=5.34s min=1.74s med=4.33s max=10.83s p(90)=10.24s p(95)=10.25s - http_req_receiving.........: avg=42.49ms min=67µs med=49.1ms max=68.87ms p(90)=57.34ms p(95)=62.06ms - http_req_sending...........: avg=341.91µs min=215µs med=341µs max=724µs p(90)=443.99µs p(95)=514.79µs - http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s - http_req_waiting...........: avg=5.3s min=1.7s med=4.27s max=10.83s p(90)=10.24s p(95)=10.25s - http_reqs..................: 37 0.771121/s - iteration_duration.........: avg=5.34s min=1.74s med=4.35s max=10.86s p(90)=10.24s p(95)=10.25s - iterations.................: 37 0.771121/s - vus........................: 8 min=1 max=8 - vus_max....................: 100 min=100 max=100 -``` - -In our use case, when reaching 8 virtual users (~0.8 requests per second), some incoming requests cannot be fulfilled before 10 seconds (`DEFAULT_WAIT_TIMEOUT` value). -During this test, CPU usage was high and memory usage went from 315 MiB to a peak of 788 MiB before going back to 307 MiB. - -## Merge - -The Merge scenario is the same as the previous scenarios, but with a [gotenberg.pdf](../test/testdata/pdf/gotenberg.pdf) and a [gotenberg_bis.pdf](../test/testdata/pdf/gotenberg_bis.pdf). - -```bash -$ k6 run --env MAX_VUS=100 --env BASE_URL=http://ec2-foo.eu-west-1.compute.amazonaws.com merge.js - - /\ |‾‾| /‾‾/ /‾/ - /\ / \ | |_/ / / / - / \/ \ | | / ‾‾\ - / \ | |‾\ \ | (_) | - / __________ \ |__| \__\ \___/ .io - - execution: local - output: - - script: merge.js - - duration: -, iterations: - - vus: 1, max: 100 - - done [==========================================================] 1m45.9s / 10m0s - - ✗ is status 200 - ↳ 98% — ✓ 165 / ✗ 3 - ✗ is not status 504 - ↳ 98% — ✓ 165 / ✗ 3 - ✓ is not status 500 - - checks.....................: 98.80% ✓ 498 ✗ 6 - data_received..............: 69 MB 649 kB/s - data_sent..................: 70 MB 661 kB/s - ✗ failed requests............: 3 0.028302/s - http_req_blocked...........: avg=2.28ms min=2µs med=5µs max=33.24ms p(90)=23.6µs p(95)=23.38ms - http_req_connecting........: avg=2.24ms min=0s med=0s max=33.1ms p(90)=0s p(95)=23.04ms - http_req_duration..........: avg=5.32s min=632.86ms med=5.24s max=10.17s p(90)=9.42s p(95)=9.91s - http_req_receiving.........: avg=122.07ms min=82µs med=114.72ms max=230.35ms p(90)=162.27ms p(95)=188.95ms - http_req_sending...........: avg=16.56ms min=349µs med=1.37ms max=223.25ms p(90)=12ms p(95)=149.76ms - http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s - http_req_waiting...........: avg=5.18s min=573.03ms med=5.09s max=10.13s p(90)=9.29s p(95)=9.78s - http_reqs..................: 168 1.58493/s - iteration_duration.........: avg=5.33s min=633.87ms med=5.24s max=10.17s p(90)=9.43s p(95)=9.91s - iterations.................: 168 1.58493/s - vus........................: 18 min=1 max=18 - vus_max....................: 100 min=100 max=100 -``` - -In our use case, when reaching 18 virtual users (~1.6 requests per second), some incoming requests cannot be fulfilled before 10 seconds (`DEFAULT_WAIT_TIMEOUT` value). -During this test, CPU usage was high and memory usage went from 310 MiB to a peak of 604 MiB before going back to 331 MiB. diff --git a/loadtesting/html.js b/loadtesting/html.js deleted file mode 100644 index b6965a35..00000000 --- a/loadtesting/html.js +++ /dev/null @@ -1,44 +0,0 @@ -import http from "k6/http"; -import { Counter } from "k6/metrics"; -import { check } from "k6"; - -let indexFile = open("../test/testdata/html/index.html", "b"), - styleFile = open("../test/testdata/html/style.css", "b"), - headerFile = open("../test/testdata/html/header.html", "b"), - footerFile = open("../test/testdata/html/footer.html", "b"), - fontFile = open("../test/testdata/html/font.woff", "b"), - imgFile = open("../test/testdata/html/img.gif", "b"); - -let failCounter = new Counter("failed requests"); - -export let options = { - stages: [ - { duration: "10m", target: __ENV.MAX_VUS } - ], - thresholds: { - "failed requests": [{ - threshold: "count<1", - abortOnFail: true, - }] - } -} - -export default function() { - let data = { - "index.html": http.file(indexFile, "index.html"), - "style.css": http.file(styleFile, "style.css"), - "header.html": http.file(headerFile, "header.html"), - "footer.html": http.file(footerFile, "footer.html"), - "font.woff": http.file(fontFile, "font.woff"), - "img.gif": http.file(imgFile, "img.gif") - } - let res = http.post(__ENV.BASE_URL + '/convert/html', data); - check(res, { - "is status 200": (r) => r.status === 200, - "is not status 504": (r) => r.status !== 504, - "is not status 500": (r) => r.status !== 500 - }); - if (res.status !== 200) { - failCounter.add(1); - } -} \ No newline at end of file diff --git a/loadtesting/merge.js b/loadtesting/merge.js deleted file mode 100644 index 29e1db0d..00000000 --- a/loadtesting/merge.js +++ /dev/null @@ -1,36 +0,0 @@ -import http from "k6/http"; -import { Counter } from "k6/metrics"; -import { check } from "k6"; - -let pdf1File = open("../test/testdata/pdf/gotenberg.pdf", "b"), - pdf2File = open("../test/testdata/pdf/gotenberg_bis.pdf", "b"); - -let failCounter = new Counter("failed requests"); - -export let options = { - stages: [ - { duration: "10m", target: __ENV.MAX_VUS } - ], - thresholds: { - "failed requests": [{ - threshold: "count<1", - abortOnFail: true, - }] - } -} - -export default function() { - var data = { - "gotenberg.pdf": http.file(pdf1File, "gotenberg.pdf"), - "gotenberg_bis.pdf": http.file(pdf2File, "gotenberg_bis.pdf") - } - var res = http.post(__ENV.BASE_URL + '/merge', data); - check(res, { - "is status 200": (r) => r.status === 200, - "is not status 504": (r) => r.status !== 504, - "is not status 500": (r) => r.status !== 500, - }); - if (res.status !== 200) { - failCounter.add(1); - } -} \ No newline at end of file diff --git a/loadtesting/office.js b/loadtesting/office.js deleted file mode 100644 index 6df88069..00000000 --- a/loadtesting/office.js +++ /dev/null @@ -1,34 +0,0 @@ -import http from "k6/http"; -import { Counter } from "k6/metrics"; -import { check } from "k6"; - -let documentFile = open("../test/testdata/office/document.docx", "b"); - -let failCounter = new Counter("failed requests"); - -export let options = { - stages: [ - { duration: "10m", target: __ENV.MAX_VUS } - ], - thresholds: { - "failed requests": [{ - threshold: "count<1", - abortOnFail: true, - }] - } -} - -export default function() { - let data = { - "document.docx": http.file(documentFile, "document.docx") - } - let res = http.post(__ENV.BASE_URL + '/convert/office', data); - check(res, { - "is status 200": (r) => r.status === 200, - "is not status 504": (r) => r.status !== 504, - "is not status 500": (r) => r.status !== 500, - }); - if (res.status !== 200) { - failCounter.add(1); - } -} \ No newline at end of file diff --git a/scripts/publish.sh b/scripts/publish.sh deleted file mode 100755 index f6d2c87d..00000000 --- a/scripts/publish.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash - -set -e - -GOLANG_VERSION="$1" -TINI_VERSION="$2" -DOCKER_REGISTRY="$3" -VERSION="$4" -DOCKER_USER="$5" -DOCKER_PASSWORD="$6" - -docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" - -VERSION="${VERSION//v}" -SEMVER=( ${VERSION//./ } ) -VERSION_LENGTH=${#SEMVER[@]} - -if [ $VERSION_LENGTH -ne 3 ]; then - echo "$VERSION is not semver." - exit 1 -fi - -docker build \ - --build-arg VERSION=${VERSION} \ - --build-arg TINI_VERSION=${TINI_VERSION} \ - -t ${DOCKER_REGISTRY}/gotenberg:latest \ - -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]} \ - -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]} \ - -t ${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]} \ - -f build/package/Dockerfile . - -docker push "${DOCKER_REGISTRY}/gotenberg:latest" -docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}" -docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}" -docker push "${DOCKER_REGISTRY}/gotenberg:${SEMVER[0]}.${SEMVER[1]}.${SEMVER[2]}" \ No newline at end of file diff --git a/scripts/tests.sh b/scripts/tests.sh deleted file mode 100755 index 814d50f0..00000000 --- a/scripts/tests.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -set -e - -DOCKER_REGISTRY="$1" -CODE_COVERAGE="$2" - -touch "$PWD/coverage.txt" -chmod 777 "$PWD/coverage.txt" -docker build -t "$DOCKER_REGISTRY/gotenberg:tests" -f build/tests/Dockerfile . - -if [ "$CODE_COVERAGE" = "1" ]; then - docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" -v "$PWD/coverage.txt:/gotenberg/tests/coverage.txt" "$DOCKER_REGISTRY/gotenberg:tests" -else - docker run --rm -e "CODE_COVERAGE=$CODE_COVERAGE" "$DOCKER_REGISTRY/gotenberg:tests" -fi \ No newline at end of file diff --git a/test/cmd/chrome.go b/test/cmd/chrome.go deleted file mode 100644 index 82234f3e..00000000 --- a/test/cmd/chrome.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/chrome" - "github.com/thecodingmachine/gotenberg/internal/pkg/conf" - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -func main() { - const op string = "main" - config, err := conf.FromEnv() - systemLogger := xlog.New(config.LogLevel(), "system") - if err != nil { - systemLogger.FatalOp(op, err) - } - // start Google Chrome headless. - if err := chrome.Start(systemLogger, config.GoogleChromeIgnoreCertificateErrors()); err != nil { - systemLogger.FatalOp(op, err) - } -} diff --git a/test/context.go b/test/context.go deleted file mode 100644 index 537ae5f2..00000000 --- a/test/context.go +++ /dev/null @@ -1,29 +0,0 @@ -package test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/labstack/echo/v4" -) - -// DummyEchoContext creates a -// echo.Context without anything. -func DummyEchoContext() echo.Context { - e := echo.New() - req := httptest.NewRequest(http.MethodGet, "/", nil) - rec := httptest.NewRecorder() - return e.NewContext(req, rec) -} - -// EchoContextMultipart creates a -// echo.Context with form files. -func EchoContextMultipart(t *testing.T) echo.Context { - e := echo.New() - body, contentType := MergeMultipartForm(t, nil) - req := httptest.NewRequest(http.MethodPost, "/", body) - req.Header.Set(echo.HeaderContentType, contentType) - rec := httptest.NewRecorder() - return e.NewContext(req, rec) -} diff --git a/test/doc.go b/test/doc.go deleted file mode 100644 index edc5a4d1..00000000 --- a/test/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package test contains useful -// functions used across tests. -package test diff --git a/test/http.go b/test/http.go deleted file mode 100644 index 187ba58c..00000000 --- a/test/http.go +++ /dev/null @@ -1,17 +0,0 @@ -package test - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -// AssertStatusCode checks if the given request -// returns the expected status code. -func AssertStatusCode(t *testing.T, expectedStatusCode int, srv http.Handler, req *http.Request) { - rec := httptest.NewRecorder() - srv.ServeHTTP(rec, req) - assert.Equal(t, expectedStatusCode, rec.Code) -} diff --git a/test/multipartform.go b/test/multipartform.go deleted file mode 100644 index 00e8e552..00000000 --- a/test/multipartform.go +++ /dev/null @@ -1,90 +0,0 @@ -package test - -import ( - "bytes" - "io" - "mime/multipart" - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -/* -MergeMultipartForm returns the body -for a multipart/form-data request with all -files under "testdata/pdf" folder. -*/ -func MergeMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - fpaths := MergeFpaths(t) - return multipartForm(t, "pdf", formValues, fpaths) -} - -/* -HTMLMultipartForm returns the body -for a multipart/form-data request with all -files under "testdata/html" folder. -*/ -func HTMLMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - fpaths := HTMLFpaths(t) - return multipartForm(t, "html", formValues, fpaths) -} - -/* -URLMultipartForm returns the body -for a multipart/form-data request with all -files under "testdata/url" folder. -*/ -func URLMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - fpaths := URLFpaths(t) - return multipartForm(t, "url", formValues, fpaths) -} - -/* -MarkdownMultipartForm returns the body -for a multipart/form-data request with all -files under "testdata/markdown" folder. -*/ -func MarkdownMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - fpaths := MarkdownFpaths(t) - return multipartForm(t, "markdown", formValues, fpaths) -} - -/* -OfficeMultipartForm returns the body -for a multipart/form-data request with all -files under "testdata/office" folder. -*/ -func OfficeMultipartForm(t *testing.T, formValues map[string]string) (*bytes.Buffer, string) { - fpaths := OfficeFpaths(t) - return multipartForm(t, "office", formValues, fpaths) -} - -func multipartForm( - t *testing.T, - kind string, - formValues map[string]string, - formFilePaths []string, -) (*bytes.Buffer, string) { - body := &bytes.Buffer{} - writer := multipart.NewWriter(body) - defer writer.Close() - for _, fpath := range formFilePaths { - file, err := os.Open(fpath) - require.Nil(t, err) - part, err := writer.CreateFormFile("foo", filepath.Base(fpath)) - require.Nil(t, err) - _, err = io.Copy(part, file) - require.Nil(t, err) - } - if kind == "url" { - err := writer.WriteField("remoteURL", "https://google.com") - require.Nil(t, err) - } - for k, v := range formValues { - err := writer.WriteField(k, v) - require.Nil(t, err) - } - return body, writer.FormDataContentType() -} diff --git a/test/testdata.go b/test/testdata.go deleted file mode 100644 index fc72472b..00000000 --- a/test/testdata.go +++ /dev/null @@ -1,88 +0,0 @@ -package test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/require" - "github.com/thecodingmachine/gotenberg/internal/pkg/xrand" -) - -/* -testdataDirectoryPath should be -the absolute of the testdata INSIDE -the Docker image. -*/ -const testdataDirectoryPath string = "/gotenberg/tests/test/testdata" - -// GenerateDestination simply generates -// a path for a resulting PDF file. -func GenerateDestination() string { - return fmt.Sprintf("/tmp/%s.pdf", xrand.Get()) -} - -// MergeFpaths return the paths of all -// files under "testdata/pdf" folder. -func MergeFpaths(t *testing.T) []string { - return []string{ - fpath(t, "pdf", "gotenberg.pdf"), - fpath(t, "pdf", "gotenberg_bis.pdf"), - } -} - -// HTMLFpaths return the paths of all -// files under "testdata/html" folder. -func HTMLFpaths(t *testing.T) []string { - return []string{ - fpath(t, "html", "index.html"), - fpath(t, "html", "header.html"), - fpath(t, "html", "footer.html"), - fpath(t, "html", "style.css"), - fpath(t, "html", "img.gif"), - fpath(t, "html", "font.woff"), - } -} - -// URLFpaths return the paths of all -// files under "testdata/url" folder. -func URLFpaths(t *testing.T) []string { - return []string{ - fpath(t, "url", "header.html"), - fpath(t, "url", "footer.html"), - } -} - -// MarkdownFpaths return the paths of all -// files under "testdata/markdown" folder. -func MarkdownFpaths(t *testing.T) []string { - return []string{ - fpath(t, "markdown", "index.html"), - fpath(t, "markdown", "header.html"), - fpath(t, "markdown", "footer.html"), - fpath(t, "markdown", "style.css"), - fpath(t, "markdown", "img.gif"), - fpath(t, "markdown", "font.woff"), - fpath(t, "markdown", "paragraph1.md"), - fpath(t, "markdown", "paragraph2.md"), - fpath(t, "markdown", "paragraph3.md"), - } -} - -// OfficeFpaths return the paths of all -// files under "testdata/office" folder. -func OfficeFpaths(t *testing.T) []string { - return []string{ - fpath(t, "office", "document.docx"), - fpath(t, "office", "document.rtf"), - fpath(t, "office", "document.txt"), - fpath(t, "office", "document_with_special_éà.txt"), - } -} - -func fpath(t *testing.T, kind, filename string) string { - require.NotEmpty(t, kind) - require.NotEmpty(t, filename) - fpath := fmt.Sprintf("%s/%s/%s", testdataDirectoryPath, kind, filename) - require.FileExists(t, fpath) - return fpath -} diff --git a/test/testdata/html/font.woff b/test/testdata/html/font.woff deleted file mode 100755 index ebd62d59..00000000 Binary files a/test/testdata/html/font.woff and /dev/null differ diff --git a/test/testdata/html/footer.html b/test/testdata/html/footer.html deleted file mode 100644 index 4fb22cdf..00000000 --- a/test/testdata/html/footer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -

- of -

- - \ No newline at end of file diff --git a/test/testdata/html/header.html b/test/testdata/html/header.html deleted file mode 100644 index abba6009..00000000 --- a/test/testdata/html/header.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/test/testdata/html/img.gif b/test/testdata/html/img.gif deleted file mode 100644 index 6b066b53..00000000 Binary files a/test/testdata/html/img.gif and /dev/null differ diff --git a/test/testdata/html/index.html b/test/testdata/html/index.html deleted file mode 100644 index f87a411c..00000000 --- a/test/testdata/html/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - Gutenberg - - -
-
-

Gutenberg

- -
- -
-

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- -
-
- -
-

This paragraph use the default font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -

This paragraph use a Google font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

- -

This paragraph use a local font

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

-
- -
-

This image is loaded from a URL

- -
- - \ No newline at end of file diff --git a/test/testdata/html/style.css b/test/testdata/html/style.css deleted file mode 100644 index e937998f..00000000 --- a/test/testdata/html/style.css +++ /dev/null @@ -1,28 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; -} - -.center { - text-align: center; -} - -.google-font { - font-family: 'Montserrat', sans-serif; -} - -@font-face { - font-family: 'Local'; - src: url('font.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -.local-font { - font-family: 'Local' -} - -@media print { - .page-break-after { - page-break-after: always; - } -} \ No newline at end of file diff --git a/test/testdata/markdown/font.woff b/test/testdata/markdown/font.woff deleted file mode 100755 index ebd62d59..00000000 Binary files a/test/testdata/markdown/font.woff and /dev/null differ diff --git a/test/testdata/markdown/footer.html b/test/testdata/markdown/footer.html deleted file mode 100644 index 4fb22cdf..00000000 --- a/test/testdata/markdown/footer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -

- of -

- - \ No newline at end of file diff --git a/test/testdata/markdown/header.html b/test/testdata/markdown/header.html deleted file mode 100644 index abba6009..00000000 --- a/test/testdata/markdown/header.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/test/testdata/markdown/img.gif b/test/testdata/markdown/img.gif deleted file mode 100644 index 6b066b53..00000000 Binary files a/test/testdata/markdown/img.gif and /dev/null differ diff --git a/test/testdata/markdown/index.html b/test/testdata/markdown/index.html deleted file mode 100644 index ddeccdbf..00000000 --- a/test/testdata/markdown/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - Gutenberg - - -
-
-

Gutenberg

- -
- -
-

It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.

- -
-
- -
- {{ toHTML .DirPath "paragraph1.md" }} - -
- {{ toHTML .DirPath "paragraph2.md" }} -
- -
- {{ toHTML .DirPath "paragraph3.md" }} -
-
- - \ No newline at end of file diff --git a/test/testdata/markdown/paragraph1.md b/test/testdata/markdown/paragraph1.md deleted file mode 100644 index f30e2d73..00000000 --- a/test/testdata/markdown/paragraph1.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph use the default font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/markdown/paragraph2.md b/test/testdata/markdown/paragraph2.md deleted file mode 100644 index 0a02537f..00000000 --- a/test/testdata/markdown/paragraph2.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph use a Google font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/markdown/paragraph3.md b/test/testdata/markdown/paragraph3.md deleted file mode 100644 index 7e43a3b6..00000000 --- a/test/testdata/markdown/paragraph3.md +++ /dev/null @@ -1,3 +0,0 @@ -## This paragraph use a local font and has been generated from a markdown file - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/markdown/style.css b/test/testdata/markdown/style.css deleted file mode 100644 index e937998f..00000000 --- a/test/testdata/markdown/style.css +++ /dev/null @@ -1,28 +0,0 @@ -body { - font-family: Arial, Helvetica, sans-serif; -} - -.center { - text-align: center; -} - -.google-font { - font-family: 'Montserrat', sans-serif; -} - -@font-face { - font-family: 'Local'; - src: url('font.woff') format('woff'); - font-weight: normal; - font-style: normal; -} - -.local-font { - font-family: 'Local' -} - -@media print { - .page-break-after { - page-break-after: always; - } -} \ No newline at end of file diff --git a/test/testdata/office/document.docx b/test/testdata/office/document.docx deleted file mode 100644 index e745b3e5..00000000 Binary files a/test/testdata/office/document.docx and /dev/null differ diff --git a/test/testdata/office/document.rtf b/test/testdata/office/document.rtf deleted file mode 100644 index 02f93cd1..00000000 --- a/test/testdata/office/document.rtf +++ /dev/null @@ -1,2196 +0,0 @@ -{\rtf1\adeflang1025\ansi\ansicpg1252\uc1\adeff31507\deff0\stshfdbch31506\stshfloch31506\stshfhich31506\stshfbi31507\deflang1033\deflangfe1033\themelang1036\themelangfe0\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} -{\f38\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;}{\f39\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;}{\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} -{\fdbmajor\f31501\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhimajor\f31502\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0302020204030204}Calibri Light;} -{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} -{\fdbminor\f31505\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fhiminor\f31506\fbidi \fswiss\fcharset0\fprq2{\*\panose 020f0502020204030204}Calibri;} -{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f40\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f41\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} -{\f43\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f44\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f45\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f46\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} -{\f47\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f48\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f40\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f41\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} -{\f43\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f44\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\f45\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f46\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} -{\f47\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\f48\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f420\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\f421\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;} -{\f423\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;}{\f424\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\f425\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\f426\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);} -{\f427\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\f428\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\f430\fbidi \fswiss\fcharset238\fprq2 Calibri CE;}{\f431\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;} -{\f433\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\f434\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;}{\f435\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\f436\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);} -{\f437\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;}{\f438\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} -{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} -{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} -{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fdbmajor\f31518\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbmajor\f31519\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} -{\fdbmajor\f31521\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fdbmajor\f31522\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbmajor\f31523\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} -{\fdbmajor\f31524\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fdbmajor\f31525\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbmajor\f31526\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} -{\fhimajor\f31528\fbidi \fswiss\fcharset238\fprq2 Calibri Light CE;}{\fhimajor\f31529\fbidi \fswiss\fcharset204\fprq2 Calibri Light Cyr;}{\fhimajor\f31531\fbidi \fswiss\fcharset161\fprq2 Calibri Light Greek;} -{\fhimajor\f31532\fbidi \fswiss\fcharset162\fprq2 Calibri Light Tur;}{\fhimajor\f31533\fbidi \fswiss\fcharset177\fprq2 Calibri Light (Hebrew);}{\fhimajor\f31534\fbidi \fswiss\fcharset178\fprq2 Calibri Light (Arabic);} -{\fhimajor\f31535\fbidi \fswiss\fcharset186\fprq2 Calibri Light Baltic;}{\fhimajor\f31536\fbidi \fswiss\fcharset163\fprq2 Calibri Light (Vietnamese);}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} -{\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} -{\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} -{\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} -{\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} -{\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} -{\fdbminor\f31558\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fdbminor\f31559\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fdbminor\f31561\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;} -{\fdbminor\f31562\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fdbminor\f31563\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fdbminor\f31564\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);} -{\fdbminor\f31565\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fdbminor\f31566\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\fhiminor\f31568\fbidi \fswiss\fcharset238\fprq2 Calibri CE;} -{\fhiminor\f31569\fbidi \fswiss\fcharset204\fprq2 Calibri Cyr;}{\fhiminor\f31571\fbidi \fswiss\fcharset161\fprq2 Calibri Greek;}{\fhiminor\f31572\fbidi \fswiss\fcharset162\fprq2 Calibri Tur;} -{\fhiminor\f31573\fbidi \fswiss\fcharset177\fprq2 Calibri (Hebrew);}{\fhiminor\f31574\fbidi \fswiss\fcharset178\fprq2 Calibri (Arabic);}{\fhiminor\f31575\fbidi \fswiss\fcharset186\fprq2 Calibri Baltic;} -{\fhiminor\f31576\fbidi \fswiss\fcharset163\fprq2 Calibri (Vietnamese);}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} -{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} -{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}} -{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0; -\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\caccentone\ctint255\cshade191\red47\green84\blue150;\ctextone\ctint165\cshade255\red90\green90\blue90; -\ctextone\ctint191\cshade255\red64\green64\blue64;}{\*\defchp \f31506\fs24\lang1036\langfe1033\langnp1036 }{\*\defpap \ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{ -\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 \snext0 \sqformat \spriority0 Normal;}{ -\s1\ql \li0\ri0\sb240\keep\keepn\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31503\afs32\alang1025 \ltrch\fcs0 -\fs32\cf17\lang1036\langfe1033\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1036\langfenp1033 \sbasedon0 \snext0 \slink15 \sqformat \spriority9 heading 1;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* -\ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv -\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 \snext11 \ssemihidden \sunhideused Normal Table;}{\* -\cs15 \additive \rtlch\fcs1 \af31503\afs32 \ltrch\fcs0 \fs32\cf17\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink1 \slocked \spriority9 Heading 1 Char;}{ -\s16\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\contextualspace \rtlch\fcs1 \af31503\afs56\alang1025 \ltrch\fcs0 -\fs56\expnd-2\expndtw-10\lang1036\langfe1033\kerning28\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1036\langfenp1033 \sbasedon0 \snext0 \slink17 \sqformat \spriority10 Title;}{\*\cs17 \additive \rtlch\fcs1 \af31503\afs56 \ltrch\fcs0 -\fs56\expnd-2\expndtw-10\kerning28\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink16 \slocked \spriority10 Title Char;}{\s18\ql \li0\ri0\sa160\widctlpar\wrapdefault\aspalpha\aspnum\faauto\ilvl1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 -\af31507\afs22\alang1025 \ltrch\fcs0 \fs22\expnd3\expndtw15\cf18\lang1036\langfe1033\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1036\langfenp1033 \sbasedon0 \snext0 \slink19 \sqformat \spriority11 Subtitle;}{\*\cs19 \additive \rtlch\fcs1 -\af0\afs22 \ltrch\fcs0 \fs22\expnd3\expndtw15\cf18\dbch\af31505 \sbasedon10 \slink18 \slocked \spriority11 Subtitle Char;}{\s20\qc \li864\ri864\sb200\sa160\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin864\lin864\itap0 \rtlch\fcs1 -\ai\af31507\afs24\alang1025 \ltrch\fcs0 \i\f31506\fs24\cf19\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 \sbasedon0 \snext0 \slink21 \sqformat \spriority29 Quote;}{\*\cs21 \additive \rtlch\fcs1 \ai\af0 \ltrch\fcs0 \i\cf19 -\sbasedon10 \slink20 \slocked \spriority29 Quote Char;}}{\*\pgptbl {\pgp\ipgp8\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp11\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp13\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp11 -\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp5\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp8\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp8\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp7\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp11\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp3\itap0\li0\ri0 -\sb0\sa0}}{\mmathPr\mmathFont34\mbrkBin0\mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author Utilisateur de Microsoft Office}{\operator word}{\creatim\yr2018\mo12\dy12\hr12\min36} -{\revtim\yr2018\mo12\dy12\hr12\min36}{\version2}{\edmins0}{\nofpages3}{\nofwords182}{\nofchars1038}{\nofcharsws1218}{\vern95}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}} -\paperw11900\paperh16840\margl1417\margr1417\margt1417\margb1417\gutter0\ltrsect -\deftab708\widowctrl\ftnbj\aenddoc\hyphhotz425\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0 -\showxmlerrors1\makebackup\noxlattoyen\expshrtn\noultrlspc\dntblnsbdb\nospaceforul\formshade\horzdoc\dgmargin\dghspace180\dgvspace180\dghorigin360\dgvorigin360\dghshow1\dgvshow1 -\jexpand\viewscale100\pgbrdrhead\pgbrdrfoot\splytwnine\ftnlytwnine\htmautsp\nolnhtadjtbl\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct\asianbrkrule -\newtblstyruls\nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat \fet0{\*\wgrffmtfilter 2450} -\nofeaturethrottle1\ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\headery708\footery708\colsx708\endnhere\sectlinegrid360\sectdefaultcl\sectrsid5638726\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl2 -\pnucltr\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl6 -\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb (}{\pntxta )}}{\*\pnseclvl9\pnlcrm\pnstart1\pnindent720\pnhang -{\pntxtb (}{\pntxta )}}\pard\plain \ltrpar\s16\qc \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168\contextualspace \rtlch\fcs1 \af31503\afs56\alang1025 \ltrch\fcs0 -\fs56\expnd-2\expndtw-10\lang1036\langfe1033\kerning28\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af31503 \ltrch\fcs0 \hich\af31502\dbch\af31501\loch\f31502 Gutenberg -\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 -\af31507 \ltrch\fcs0 \charrsid4728168 -\par }\pard \ltrpar\qc \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 {\rtlch\fcs1 \af31507 \ltrch\fcs0 \lang1024\langfe1024\noproof\langnp1033 {\*\shppict -{\pict{\*\picprop\shplid1025{\sp{\sn shapeType}{\sv 75}}{\sp{\sn fFlipH}{\sv 0}}{\sp{\sn fFlipV}{\sv 0}}{\sp{\sn fLockAspectRatio}{\sv 1}}{\sp{\sn fLockPosition}{\sv 0}}{\sp{\sn fLockAgainstSelect}{\sv 0}}{\sp{\sn fLockAgainstGrouping}{\sv 0}} -{\sp{\sn pictureGray}{\sv 0}}{\sp{\sn pictureBiLevel}{\sv 0}}{\sp{\sn fFilled}{\sv 0}}{\sp{\sn fLine}{\sv 0}}{\sp{\sn wzName}{\sv Image 2}}{\sp{\sn dhgt}{\sv 251658240}}{\sp{\sn fHidden}{\sv 0}}{\sp{\sn fLayoutInCell}{\sv 1}}} -\picscalex133\picscaley133\piccropl0\piccropr0\piccropt0\piccropb0\picw7594\pich8467\picwgoal4305\pichgoal4800\pngblip\bliptag276711534{\*\blipuid 107e486ef6f08100a0e5ef6a660d26cd} -89504e470d0a1a0a0000000d494844520000011f000001400800000000a0e427b300000002624b47440000aa8d23320000000c636d50504a436d703037313200 -f80103eacc609500007b1f49444154785eddbd0db8246575efbb7262040b621cd268219881421328f08bdaa8052a6ac6463897464be2edf104e3638026b9b1c1 -2639910673ce3df468f224d00de6de283dfa24eae921367e80e7f6283d86687a00ad013f426df1626d6a14c3c8901a25a97620e4a9fb5b6ff5deb36766cfcc9e -99de63bcefc3c7ccdefd51b56abdeb5debbffe6bad5fc8e53fc21a5be62af47fc5bf931ffcccafed17fe63c867b11c7689e63f80907eb6f25910c058eeb8f337 -dffcdc1d3b67bfb1f345082b3be6d8d3cf5d10dacf504e3f4bf998dbdef2f0830f7d55c663230ce7684945b61582b15f75c25197ca71cecf748ffd2ce523e3af -8c3e3fab62199ff2b29973b03aae91c5ece71e7f56b6e5fe8994fee081932f585398a59fc1fad9c967c77d7f886cfcf79e73fce4cec7df90a33e2772db93cf7a -127154e5e96776caa8d0abdac5ab0bd11df175e4e563cea76d777eea7e71ffcfe7bd6435773cf7c08fb73c26affac123df9bab3c38574def2a7fe9aa5bbaef94 -6d73ceabe59646b1dd7a6b8fb86ccc17e6477e656d94c1eb24f9b0ad5f9ed61a8d663fc9f863b59e57655095465415b1c4726c714a0e02e51f373cf2579ae747 -5a7fc6d6dccdbd27ac0f5ebeed810d5be5cacb1629c5ecf08c8fdde63ee77e2961ad4bcf7ae609c134cf89fd8e638d911209677e062a74a41f4acb927a968555 -c76b4579ae4a9327a346ad6c8b6b4b3074db795daa912fdd514fca6811da633bbe315176d09dbce3c85df491dc5f08635092661ed52b56bdb8c541bb55f36b75 -5f6a83ca280b9c41dbeab544aabe1a65bf24b62d96255e6d5e7110d0915d47523e79ec89d76bf96e23357ad0aeb8e55a2fa9f6f2bce70dfb65cbaddb76a7e40d -9a52cdfbc28fcbf5bc29a3b6483bc0049550a77aa172476c1d49f9f451865225e8cfdf5cab1de9cdb6a4d9eb364b7edeab64799865de20ef4a37746490456e2b -ef34f2d06ee4116fc64e5badff5fca27cbd341994d52aac7463a939bcc6cbb5c6e555555b2f230efd6c2b0d1f0a45a6153b11cc7721c57ecba235e37906ad758 -a17eeb88694f9e1f31fd51e9488d8db5b0e261cbf7da752bcb6b3ef2094a0d0c8e53b1adb61be4232967353b4b73a7195a526d067ac67bfa19ec330cfb915a47 -483e710507a6abd2319a93e5c3baefd507e62f69d812d7f51acd30cdbb18e0fa286fd4f3969b0fb0e579d349eaf843fd92d4501f3489030d711ea97564e4c3dd -cd1b0e1549d4b0edaaba7b613bf0dc52c96df707931beef5fa6c24f6148b4d6607f84096cdc664c335cb2aa33ae6bad8a447621d11f9f4448234efbb85654e5d -29238eb0e5972a6eaddd1e15f79984fd7eb5e962a29ad28e4bb5dc6b7088051c6292465627ef56c5a9aa59aadb529917e78a8be808c827f4ad5a980f1c072b6c -04513577e74973a20649afea79ae1b04cd5e9827faab7aec3a3ddbeef56ce98c3ae2653d61e355ac347524409b84b36dc54563be60e5e38b2d67bbb79e7bfb1f -cb5b6f5c141d2c462b365d6cbdfe752f78cb2a7ebde3feaf8de56e916fc9d59f4f9abdd9b23c3aebbc42eed95622a67fee334fb86fbd6dae71fae59dbfbdd7de -7c8470a1157f0ce54e9e04ee52cfbb50812c336a9474ebf55aadde1a68a01a77f2969d0fad248fa4cd2feb12c7d249224fcaf8d4b803aeb82b7ee147447fc68d -8f0ed64b6317568a96ec0575cd0ebfbaf5c4eaf37ec3a8c4d6edf2cd7f7cfce9af3e511acb73adf136a9c8ce7fbedfbbe646f9aa58a756d76d686cb3ad3991fe -2547245a5de1c730c0a1c3eec4e505af79af2f8c6db7da33ba14f7dae59255722b41ad396cc92872a5def288cd3a1d9f13ace4551d6925810426223b22367a85 -ed732442b095544bfb0c2c114cc8bf5934082ccbaf7587f32e6462d5fa8e10928957ef46a1354a4b526d10e6071c625ee872b41d011bbda2f6796c9d7af4179c -f1073e71c575bd73f60d908ebff2896fcbce371fb3d63580fdf6d1ce7fd2adf3edf4e2a37a4fd8b7ffd1bdde566056ebac9154d6ed3c23f8cc8ed64dfafbea86 -23b0c156543eb2f6ebff68cdbeefe8b623eb7e5b91d4bdd7d67beedb24a79d75d66b56c9d6d94f3cfd8c95ca8cbcf26c11834acfc9dfc9653bbeb8f6e6e6578e -77d77c60f43e293de1fceda98f5d340788268fea91b7c26b45e5b3b9fc8fcecd733577f36ee679f11ded78ed6ce5bd2ffca7ed3fdafed043271d7bfce972b946 -e98509dfba5a6e96abf46fb3e77de2820d97cdae96d9f153df6fa5fceee859fb99272a77acb070f4e357d23edb56d51fe45138dab7a14830224dbfda6a119ced -7a55590f75afa93f89db38dc9d3cf789c51656967690a37d04eccf4acaa7818dc0779940854b3c8861931b07f39938d69357647987b7e5f990302bcb5be5bca3 -be0e48505ba5c6cf09482a6de9108aad7c1cb682f2198905a4130cf7891973509bd8abe538be8615f32bb28afbf6471ae827c4f1001a819b3b957cc8a78dbc32 -015925cc1db1575c402b281f5fe2cc6fec63ff0eab713e6846003c665bb9c61dcef248a11da00db34cc6876d95795694678906ac0df3ab063a85566a988fbc56 -74ad9c7c46e2b8ce9e5e21b79c0c1b7e8bd8a1978f1c37708ba0bee316b6a453421ef3f241409e97e5e858b7c87d4db02342fa6aee257915d0c3e92fd6bce98b -6ae5e45317bba1d677f7d5c70526882a102e7647d22a616c76bd2ae16f03d18da5cb453c7d7bb1680a70b52d9e93e4a59a2243d68afad12b269fd472777bb27a -c359cd937e1e3af5b83c2f1180697b2fdfba51dc72d65620ac56d9a53b3988903f1aa9c5b2f256292b75816d8d315fa9b552f2c99a92e69ddd0fa628eaf49b36 -e186d5256a5fb8a37a2180f9b548e3ea258414eade8a1a4ed0f0f485435f6cb7d4af896f85a995541b156701ed5f0121ad947c72ab1cd6e7c5a3b73c74067d4c -4dd62cb381165bd51181a606604bdcdca8146615af14341dc08d407a650ff4312bd51c3befda23a753e984d26dc84aba412b251f20553f318fbe5880a3ba63f8 -b7bcbb418dadc2f133fb6f09196180d893558e7cbf1293e671929af49d3873b2b267f7cab5bc66831dad988856463e591e483ba9799cd6a1c99442c49837134b -de0a368524eabca51ed6b2425ea334ab770752cd72523e6dc9f038251e49ecf6e24a5e4e013946b96df365ea16acc85a19f9e0e0d6faa57a9e36f4f86e97aa49 -dcdadb8c660df659cb1c485541243d2985792725f71590a3e7a74ddb69e879479ec7f222d74afca02a21079a061c61392bd94ea922fd54f3402ba4412b249f81 -58018eaf5b4e914e9953885060f11d74d4ebe928765a96b2d11dcd1b87b5b0e99371c7cc94cb79c246ec492fd380154207d86adb4eda32ecd7f25223e8fbedbc -d4eef3bb514dbcea4a111756483e758804690d1f991dd1ce47b1b9d9454b1a88ab2e3efba2cd06c987bd06b2422cb835e4e3d543663f61df7110eb2a83bee41d -b560301c709aa45d915a3eb0d2b6905a336b8572862b211f8c07d6a64306b0532397850b53db259a913ee9ac528ae218fba1e14284fd6d0e63e81b515b6ccf72 -482d4b3365bf3929f2cd6b529630b4541cc8aee1f5116c5a6ba279ecac3aa14991989fa4d1a66c8556423ec403326a40b9f06b83a8c475a7bb5c5c9333563cda -b2bc0249ed678e115f3ba897e001f15e1c6feeb73e08710f4b7e868f6c8ff0b2eba581edf7f351d8b007b96fabd4a592b7fcbcdd40869a205b81b532f2413186 -19587bcdc384068be19fc0e486d51461a2920192a9d6084e89f2db5645e23ece10dc280bd4591c0d5ebb695aee34e2b28fb52aa1476cc541b59f27b64443afcd -dfcb44c1d6202e493d594c7e9896a856423e59eeb8588f3e3ba735acc496b5c8418e04cb6256db71aa8aec8c90448b03ba2e258e32cfcb935e18e7b12df62ebf -38e460534a998b08aa0df2af24518703f1907519eb35b00c79467f3bedb512f2c17e569bd8e62c6fbbb9dfe6fc59e42f876c1610b12cf7a3a814a8d5d05b6bb1 -ad8c140793883f4b076a50462583f0b44b7aeb2dceb89172159aae135998ec246f7449c6d7342def0c356f38f5b522f2e9908031f7d916bf120dc2c662188b3c -06acc2b48d3fe47a1a86633b70766aba77f6586cc3c93b177c83a1d7842ad354c97b6d74302221cf56f5cba5145ee7d4c5b302fc286e0513eb39ec919ecb39ae -abb83df35fb2582d7807031b01628dfd52a824bae5ad8994ba526ec8a82971bdece651a593678e54ed00cd5cdec71cccaba6ac3fdc4107b36bd55d927e10511b -3d747eb7701422582dcecb166777937d97f8bbcefe7dbbc07bfc6660893b0ac4ade60e2763c3ad28e7c5eefc1cc84775a70e4dd5375678808b53e84d48801557 -3043c4adbdaab41cdca2d89ed79b838e0d42cb52b770900f3c1fd72977876c69cda84e7d4d597f30cd6ea782c70b0f37eaa3221a46a52dce1ed7ab340bf9446a -9183c1b088410f7ee9bbf8186cb27e38106e34741a31560dc876ea6bcaf289a35a47b316357dbe544c68040f3f1e43ddcd5d9f3d87a908625c3e47cc9174c8ab -aee519b541ad6ad72b6297ca84f18726ed035cc174e553c6aaa87c081ae238e6f8c576e6a36eab4210ded72d108c9a9635c0e5f13b87753bb807ec2f0b670aab -e3e4412527ba3f6461efe78d53958ffab33870868ad96f542c409b7c54ad87bd404971564d99719d6e755412f089c35b9946bed5520fdfb064d7050eb570624e -7f4d513e65754574997c68bbdc1d6a205ecb809a9b5d88839ce4153887383f267a3fec55c5e2b8bdb21ab7ea088f54cdd1143e76f7eb9a9a7cc0411bb8c1bb2d -276aabc4bc8050b22c50525bc0a1f54876cf6c1caaa0622c90a59a1857ab399954772542f8a9c987bb4c01c122c2e8ee04ed54f7d7b8d1e8ff50aae060986b2b -ea2fe1291f928ce29ee5f6f514ecdab9d7e15bfe239f5f0843e5a13e3e3126ab65d212bef4717d280ca8934e8fe0c47bb9bbaf9cf3410b2929bb04697dbe6438 -2a654322e2a9afa9e94fccde22202563354acc7502fcd486795a116b948feab82976d40df2c6a04b626c4a2baa91e3ef544212400d71cbd298baf9995efcd505 -29c66f6be62199713dd53d50c4167fa84aa519e661103a79b59b7aa13535f5c9814efcaa3de82b38044652406fd35d53d31f922cf14801195642d83db232dc40 -2aba86a541d3b1d32e58aa17f6aa2d346c5a2b23c0cd6b8311b16e2d48f01da6f5c1bb3ee73f4d8da2f6c9a1fd9dfbaf1398941bde3ef7ea0de7daa7def31efb -9eaf743e36fec41fc6afbaec7449c75b9fbaef859fbb7a6adf28967b9ac8c57ff09d677e67ab6c7dbfc819d3fbe8854f9a8ec807a5b67e5023483bc180730433 -9d8ef09f414ac93858516a75bc52a6becae185157b5c6dcdad5a51481db871a75722853195fd850368f2077d8f2ac84ace59d593a0dccd1b9ed389bc342c95c2 -ba974330ecf970c0a6e6c4b177f1ca83c8763c9264c6159afa9acafeb2e4b55ab335f77b9ffe9fdb64a7b46e99f935ff8ba3abce97f7bfe2a28b3e31fba1edaf -bfee94ad4a4abdcf3ff38d26f29ed25a539375cf9cf107e3f3066319fbfb64c91eceb74d49e26a982b5ab0859a77b5cab6d32cb7f2460c552c4a347ef4474465 -930ab0297da77e0c8adb6a5914ae081cbc155853d95ff83c4ac3d4428b9606a2cdb03aec20ad612fafab4b92e037b74601dbc1ded74dec0283f40dfbf76316fd -b65bf65c12cf0af12fca414e514ed3928f8d5d51dba261b56535417f60a4940bc83dadc0fca9128af194fbfbbef74371ee3c1969fe1070cc9c0fd35fd3914fe6 -289e1a5000a9e788b896554dbc3499e4bdeacd663307d368c0d8dce71d0cbbdd5d1962b2f5cb935600bbc1ad4967c568acd3918fd6ab85a1f855abd410cfaefa -9c5681021bba9260c4c93b0222eb72a62d79dfb1a9feb6e83a316ab6da01f2b596d6876c341c7608d7734318c2e4959be400a674134b3dbbe97c74224d1f9ec0 -90d454ddaad404caa5268753f3585b5de2ae5a40ae67b47402062687dbeb6add92652a78ccb26c7faf84588a75d3260a65fdb0b4d38553564a4b887365b8097a -f1d3914f53ca54ac352940526681df9de0409868d4c51d06ad9e5dea7859b08f04deb052aec0f6f0dbc98830b3d2198d3ab8950082a629d0fc1ae15cfa0d1560 -a91a58b60aaa13485745ba22d0aaf9dee9c80740b93d94b0011e3cec25791f1048933a25bdeea414b5878157f2826cc9e71cd54a52098240b505023dbb72d42e -7b45471b93619f2cf5c5fbb59245983e084a41a39be4cdcec0538ccc29c0959558d3910fd69745a3957ab95e73da75dd57308094d094c7f04c9b02f399ace912 -77800303bd5017841fcf777daf52a981e7b3d99ac3f62e249642cd3a99e876ad2c15a712e5ed668bf2829474231d39564232c5674ea9fe6be67ef1ef1d3aa736 -9e79e8827ff8ac44eefa1f7dfd8eb54f7f86ab9f3ddb5ef5ecffb1e69c7b7b5bbeb767c1d6d8dafc5a71be604a0b29f9eafce045d90bdee4089ddac8f6b58e7b -43f8e1a3afbfb82808bbf616f6d539673e78279ece4d1a4c6cb73e77cd7d4fddb1cd7ec7cd87e31f1fe8bdd3117d40f05322c9a21879a08c5d90f83ca812a202 -d2d032a3cb6f5b81d1a7dd5699b4b0d91bea1f66be5b2ed92e079d5f759bd8f73ef1ece43d11fad4c96a12e024b4543dfbe51efad6b45726adb3709153d95f4a -e7f50750275b5803dbec891afe2c65c9c887723fd7aa0e65501fecc543c8827a615aa37ebf5ff71d8703d02e572caf8a55f1e9ee924cce7172f9982207f75bd9 -882671daf592301892195cd13515f9688f230e910669778d3eab463e55e463003d6c099da1e077ed6d4293ae3f54ed69d27b8c7dd3836d60c0d77ea7dfd19fd8 -6136314e8140a4ea40d9abf7db5504e92ba09292fb5a09506391c4a7241f341f32c600e30abd520154958f57e47c1be47fb1217e7b4f339a9114ae1a79c49c43 -a63154d4aaf8956aa7db1d0e6dafbe0b308dc56f82c5d74c3705d6b05a55e25d9766402baa3e533adfb946f2bc8d7e3c52ff4781e801fb0b5331548de0367b1c -70d4b8ed792f5ae034ec342a25f191536412acf3ab11a64d41bdcc0ad125f506b5f06930e854cbd5c0c7c9eeb00b57e8609f5cea94f447ed81b939abea14340a -48975d714c6d0ea03df89eb5a473d883bfd96e9660cf23d2f9553484a4cf166a55281db4867e5e6e28ef95e043bf66382cb9b41132a66805d7f4e4a33c0da756 -e9551d2e3941388088d64240aa71ebde0a343149b1b603524f86d239637f8da4bb79d5c69e157c57086388a72c2edca8a2b5263ed280a3ce1439afdc9aa67cb0 -b375077f1f93590e234913bc185c68737ab760b4ec857fa6b64ddf1fd7212e55526fa56f970dc286a7a3ff69e690e3279c27f83de5bed5a8b9ec34158f9e6115 -28ac2bdd8563baf229d72b71dc2301581de6b075cbd2a94d72bed1925b61685b9e5785ea123b2e84578c98453f12ee3df62a6185bd99da935a66cddf934eeb8b -5bd34e5b1ebded42a715cebb472ba640d3958faf54672d1fed74f34a00f859a9ccdb0714640957653eb298bfbf616f18b8cd067418a36ccaa79ad056101d02a4 -02cc97669851c05a2e3971835ea52b261af3c153940f575a56d8a6450a63e043cc016fe6d8995c3f71d672c2ec263d587112cdb63207fe0257c8337969887576 -bb6559696657d8cb8b99d52b22a829ca871bb2d59a764bda508dce85bd3c85b634ca537ce424414007a8343650351d6c945d366f75e3499332b5d066ab0ee820 -45e38161c9d1c2b0156064ee2ee6a9ca073a7814e4bd019b033e82c287d0a2ea791783dd7233ea6c96f388b35e579d9ee16ebd24f941436a436df96bd610c707 -6af8b23e70395fbacfd74c553e23695351d1cb6004566b8506d00413e7a7cfbea08ecba668697f6b580a346243acfdca8444b5eb0d51c91a806890d1a786d96a -472570e7bd29f787258c25de3c55f9e4de2868e75d4e99a4bf10abc3e26ea3407eca61bcff1a24d2b0f491f402d0536b09ae53888b3868c1d773a5e7d06f41d1 -b815b6ce53b5cf18cfd2c8cd479c3c36156eeaf872f9714dd294b8804e1c5def800500541f4cd612871dd50466b912453496e05ba6ad2d7b7fde54f2cbf3f774 -dbea2b2e97f0f9227f70d7aca3d818aeca4517fb7fb1eaacbf043e93cb6edeb671218658fa0fabdee57896ebfb96fd577bbfe0b444a7198cc171cfb8a1f982f7 -ee985e9e7a3f5735c54740590a4fb4d9c1df736b5dcb981f14aa5fcad96d233f2541b89c24274147aa3ef85eab638c81b64b1cc1d850d069c5b7d734fd1ff576 -40739a1e56666469553fabe5e5915632456909a254c7086d196b8f72dee21db1b5608f13fd53e7400ec332bee8402f99d6fe42f33bb312ac919e9cc85082735f -fbdd0aea3496c74f14973e73a77d6ed5b31ebcfa0f67c69b954075e0b57aa96e6e8eb579fecdab7594c18cac3ff0271de62ba6251f6cc1f692dc206ffff84e73 -45977f50eeda8afd79eac54620277f5c5e75f759b34fca96c361b7bce4abf326c7c8e904b96759c23e1c114d4b3e4861dd1565f7e2cfbe27b954afe79235770a -c362c68fcc2090b1ac1dcbc9f7fc0629bf5b761cf2d58e65f537e7dfac721a3b956ddb0ff9d396f9c6a9c9c792adb75ebeeeceea39a27d5199575591f79d2fd6 -b13f5a374642332fdbfaba2776fadffde81c11c7212e4b5eb3db3b2d79933c7c889fb5ecb74d4d3e229f7ae233d73b1bbe75927e37cff7969a8c66e5b14f7fdc -ec89d76fc77abfe3536bdcbb38e20f6957f0a6bd67b97d78d9377a882f9c9e7cc61f97dbe4cf648be9e8c85afd91f6f80679c49f48e3e1b17ceeb5dfd9fcb8d5 -59bff6901c17de74d21c2cbd45eb5cc87c2bbca6279f5be708402f99fbca1fcd0b88c6c4b7cd9e685aa9b27ec4bfdbd181affcf3e5df3e887bda4dd59e278a1e -2eacf1c372ca417cd421bd747af2f9943bdbde207f77ff3d3534c6dc96539359f995a3ad0d1cc3e317fc588e595d7ae5ea53df2f49a105cbda65935972c5bdbd -44ee5d7c93d62fc9638774d307f1a6a9c967c3fdb3eed5f0e7846cba25d76ed0dbbf58feedfc6376ca1c98f2ddbff63179c1d12fb19efdd8252d2318b5da075c -e399372f7ecd6aeb9f164975f6e2634a9b9627e5037ed13e5f302df9ec20ef70b9c8e6bb7c3cb7b17cfad7c4da7cfb7972fb29c15b77bc40e4ed0f9fc460c15f -3c7efcc83f6d7cb56c976bb658070ac55486afbfffa3bb09c0deb458aa775ef2c4e9cb91f2a14b87771ec8c15ee6effb0c322330aa16b8714c1d6a0e259cc3bc -d7040f4abafcd229fbcd88b62b437e694794ab1c68d1df6f0f40a4e86e629602714b42da07fad483fbfdb4f46783f58273003c5f236643fce22b568de5e6976c -b9c69239ebafad736eb9ec9dfcf4ae47d67dec9d32f335f91bd9c69c34ecf8fe4d1093c2747edaa21735e8073dbf3e7bd55a5b1e3d2cdd58ce9b0f4e9cfb7cb5 -5da61d3171a94232ed3ca9818feae425abd9ea3940f53d27e3e819256483a1b542ee89e9bcbeff723f923e64db778fd0e3453990ae30784699632bbaa6a43fb7 -6f3b65ab9eb55bd4fa6c1c1fcf1f8772ee83627dfce397cddd7ec19a77ca53f592f39de13dd68ea38617c96cdf412b6edfaffa581c4e9fdac3bcec340ac3dbae -3a472e736eba5407112f47090efd3553918f5ee3479f38992b27809cb38ed3bb7aceb61dc73f3afbc4297f39964fca1dddb9ffdc78e72bb66c11f9f257b22d9e -7c4312917b0e70821d25f79fba71d3ed5b37cc7426bbec03a59d7c1732a9dd7ba5fcd92d171599d4155dd3d2ceaac14e991098e49504ce2a0d3453bf0b8b23a1 -b230cfac3274b22a19b04127ae41131ad5ed4ae9009b23296edef13c436b011073ab06624d86603f8952a5563cbf3315fd310ff01cf3dfd3b75273342b2ffb13 -b9fdf54ffe68a3fc4456bfbdd4da945c799a7cf337ffde5e2dc7cf398f7ceb4f3a576d7affa507da1aabdbbcc2b6df7afe35c1fbd66d3cb5bbee9bd7df76b931 -d77fb2e3aafa87e4bf3b82ceaeec9a9a7c9e3e69e1a0a15aff779f1e5f70ddeaeab5ded3b2e9b82766df3c1667d399afd8f60e6ee62979fd9af02ad979d5a307 -928f5c1db7abc7bdec231f7ce767e5fa0be7ae1e7fe2faded5aa53abdff81772f3239b56bd588e3ed01978b8d29b927cccad62178e97d56f92576f913597feee -0ed97ce6cccc25b2c6a5579dfbf9b90f5fe91a80e21179f50fb9c557c84358a1fdafb173f586e84beb84314ebacfcebc442e2ee0a3dfbd6d4e2efc30d11767c2 -ca1aa029c9e7a9ebbefad4e452bf20cebf6c94b77e7b46fef27179845d77f9712f1fdf31b75e2e90da5bb8b76325b9ff4c91cfce0de5ef0f201f6e9d51b16f93 -3b9fd1cf9e6bdc7e6defddb3a8e99c53fdbfe4f207b1f66f3a5cfd38d0fba7249f2f7ff009f34d3ff9a97c47e4bd37c8eae8efd6dff6d64da780231ffbede3e5 -e58df12d625dc24cee2fbd8e50ded9f4afd6abdfdcfe7f36de7ea0eb832cf6efd66d4fd4472fb7ac2f9c7bccc7e55ab1b65e34b7eec12dd6ab7ee71f3ccdc1af -ec9acef93528b9104da1ad0e73f2e6a1893238b298134725bc4b9544ac0907ea3152da1b7bb451602c25354f93dec6fbb906bc44880de6d4a2135756b5753863 -af9df7ca39731b57a0e07d8f6b9992fe644fec4c2d18f0bffa6ff2c86372eae8f18d6c83ab371f7d01fbe2dc9d9f952f3b3363d9fae0ff217ff1fad59bee7f9f -1cff244f7d3c1e3fb4df87bff9aaf5eb4e3df583ade65fc8fa9b65d3b06355b63d01aabdf6a4d9b5c76e9c79eecac33f538a4f33525145baab9557f0765a1011 -48ba8f92217ca99e05b95b7fd9243bc8d4ee409fbae35a2ecc7af3f3a516ae51544678aee3b4b5e152e6f4922ee5abcd96f6b40de9791bc0d15fb1b2b8854b9a -52fcded34bed75f32e0d5161c9d36eb8639ad824b44aad0a13279598c2bc0f56acf4f1bcdc6c48936369df0c0c08c38cb18a6890a859d72c745d38694997de6f -70c6e89b5ccf2a5cfccf49fcf56b42bcf9d797ef38c792b7fd40e4cc1fc9bbc2d59b8927ded391e7cb43162ea35cf7325380bdf3ed7a245fc090b39d8ca94a96 -7660b674d65edd7955e5ab331ff8b3d572e2511c5f33b7ceb677bcf743a76f2d7deb965be5b4fbe4f9eb7e289a615be1351dfbacada28ce634b1d1711e92520e -463d8f1d01e023345303c8991f708f5e0dd3a4914b9029e5722915c0f45a61c0e60bd4d06be323f4843ae5b49555a85776ea56a03ce0e5f6953c8c7b9c927d7e -897c49e4986319db352bbfba41665eb8452eb969edcce96ff95399fd0954efb77c596e7feb0ce90760a19367c5394a1e94ca67e56a3966ef6984c4271750ce2a -cfc6bff97340a22b67e528d593f75b77bdfbf7adf7bee53bce934fc91737531977d60aeb0e1f3f25f9acaeddbf5534f375d28df2873f184bf04959fbcced97bd -ebc15fbd8f7aa5639f4d1f910bd7cdbed47dfbb6f1d50d57be66658fca1bc57a407eb0d70e01dcffad27c67f33be72c3ec1648d3736b3fc9e8c1f53b64c737c6 -72e765b2e6d700d78e1a9ffedbf7cb732ff8b9910f49d37bc47d3e79e5746ed587be21e73e7fa3dcf1df9237df75cabf6f10eb5f7f79eed5c8e163afbdf4b3df -bdb57ce3d6db1f94a35f8c38e71e93f9f4cffc9d2a6c7fe9ac0c67aebc542e3b7bf3dccd2fbd0d819d77f99baf9ce93079f95f65075dd99e7b8b9c880bfee4ee -d9b01511d654f487e8eb7972a71cfd150d1ffe48561d352767dd2072eb1fff44b67cec7a2cf4b146b7ee5df7918adcfd2772c56f9d22297d38c4f9cf72279b68 -f1b2765cf30b5bbde69acde76b1aedb52f35f1e8f8accafd1f9d93575c66fdb35cfb9d8fbe7d9bebbd6a0d1d57d5875a11a92cfad0c3b05dbbbdb50c598931c2 -79b570692aa69f0bb5a5740c70c37205ef6808557c401785a1ed32dbaa0d7f304cac3d119c26154b75c8515a9951eae88c231550330e11473c2859d8e78e5b67 -82731e1c083e9aca9d4d457f54dc7f2277c85164bd7ef7a7b28387faa6ca96add79c4d79e4afbc57d63c75d7a66344bef01ad9f8dd477e4f3ebced2d8edc7092 -64e51b565f5afbe26205da72fe072df9c4a3efd9ba69b51edcad9e3cf9117bdca97df0dcfa29f629a7beeb1332fba6e75c74f9536bfc7f91ad4f1c28fc9f8a6a -4d45cafa21ae052b99ff7b695d79e00cc36b0f280aa06d005372701f231a9205ed721e594c83815f9fd5f27a3aeafad57c81d58c0f35b0bb899dd795991a4b05 -2159ea5536618a51d461457e377650c476a3099373e5ba02ec12cad4f487ece0cd6be41a909dbf79c72699bbb8f2671bae963f9571322b6ffea494cf918ffdf6 -b8fd3a394d6e79cb3b2db9e37f48b25d9ebaffdc47def375f9fa2dc5939e3d8fb1dce75cd68094a158ff9cdd965af5e5df42c1d60d6ed8b14ade353efb5bc77c -6d6ee30b379d79d3adb1583f5d468af1b075686afa935214d1a57b56e8e5fd306f34fb26872325bb828f4c2ac78db220a346d22312497488538df82c6fc6ee20 -2f1b5a7c46411c55f36d42b66a8d717a9a1e0adbb165d7cbe52a762a2e97228b3e1483a8325045a31278e5e3afe9e9cfaa0f8e2fbbecf5bf2f33abcfb970fd8e -1bcf3cbb71117b4bae38e5c5f78873b1dcfe1c9744c6cbcedd74ee1ab841a48156ff57f9cea3728af3d62fca6ffd4f7dccd7de6f7dec3a474e429b1c593b94fb -82cd27bb5fdb69bdff35c7cd88f3d9534fbd60abfbf2ece36f19edd80e00b9e965f6ab343a59e1353df9c855c16737dd74db0621aa3aef5a59bbfac6d7ddfe56 -71bf2437dc28efb1e4936fe320faca6fca1780b4aeecfdae46e6f2e52df2653965fd959769e2e7f6cdfe95d6e7af904b80d48effc9ce47e4eb3795d7584fbb57 -ffe98fcefc2f00b373d6d9ebce9713ef5dfbaf47c9f32c2bf997ffedbd2b2c1bfdf8e9c9672c7f6e5dfcefcd776e5efd8edf5d7b1e86482efde3bb5b7f73de3d -bdfbe505b2e3c1b5d8daf56f92e45d72f3476f57dc6f2c67a21722973db6e5dd306010eb8debafd0cc22eb85677efeaab93fbfec35f28c1cfb86b7c91957af6a -56acb275e6bddf754e3df6d45fe6343ced3dbc2c5b71094d4f3eb449b8435efab6f2157275ba76edccc55be4dcb73e72ddcc8d3b3ea0dee3675412729ab361fb -2a19b44c206fc90f7e4a702fefb8f29bc86ed32f9ff832f9f255867829f2a2a3676f69a4b7fc127f7bfc5d0f7f4e2a971e5f3deb2d8ff7e4baf163739ff8f6f1 -d6f895979d22de7d2bee204e4f3edcca9a3becf38e7bf1ccf97fb4a9b3f655176c91173dc3bef97dfcdc4df2f1aa7abaebe4365fb67cfbb7cd631f8f1f3967c7 -f7e591a35eb351ce9bfdab3fda7ee966820623b88d9fba9b54da0df29b40923b3f72f1fdbed57c5ffd7bf73c0515d672ffefd6334f8da5bbf5a4e7cca49b4ed8 -8d40357d759aaa7c64cd16fbb67beebfebeeed1b375cf7a1b337acfbab1999fdfbfbde41fc2d6f433336138bad95e48f579bb0c0ea9d66ddf70cc8ceaa67b6be -faecb35ef2a3735ff9fb931b7cf4fe7bc5ba5c82557af7579e2eefd930eb3cebc5279ef6770ef8fc8e27e4c9b1f5c0c33f7c72e67bb26da5879c4fed7c2f3e28 -51feaed5ac81cdb4681eabc736f84d3b37bdbd8b51cca6fb8d3a94d4360fea437840bd28b6f2bec115cd8aa84528591dbab81286048c68228ddc359d37f513eb -34a21227083bfdd805775a590471bafac3a9bd216ccaf84ba7cbf5375f77f5b76ec383b3a83b3873ee59fce1ba475f87e26cddf412a325b7cf3e718f6c7ae9c6 -6b39aec61b4e976f9cb90075dcf5fc07c7cf5f73d5a6afc9777f617c921ce37f712349213e02c38d16ca8b1e93f13333f98f9de7243f27f9c1858d3f96997523 -ef876f8dcaf7c975a76378c637e30e9f33c78dec78e8331722827b76ae9e5576a2899ebe72f2af41818ab63ffe12b9a2b04aba3e35fbbe35d7bf51b6ff927ced -cddf78a9bc608d7c42bede7e19398fd5e0213704957b83273709600700c70a3358a7ad3f7afe9cbbe5a6d77cecb854acf7cbbd1b4ffdd337d3ff87739aaefc46 -41e65e2143fdd397c2ca4bce9f59a330c6dc0f9f3a53e64b52c6b29e34e21be51764cb31b2d5fdcb73e4c33f965f91ead98263943e0d74fd19e7e8df1b53c540 -bdc28fecd35716e298b67cccc174d5dc1fdc74dbe8fcf32f74efbde4c9673e098be0399c3cab5e893a89bcfa06f9f56d9bf8e38c6c7f3d4e8faeec324ef82b4d -247ff336f940a3212f78de19f24fcfddf4cb728dbbe597df72e54736ddf753d5947f79b66c94ebb6e2389f2e8ffc923ceb816ddf5f59887e25e463bde30d6927 -7bf95ddf8340291fdc7e99fb1c91a795ca749d09284f86c5f0e772f7357372c17545fae135e7ce38b2e38ee3f8e3edb73ae79f72e333f2bcefea7b3e3c2333e3 -87df7d94bfe36b6f49b4a4499cd9ff77f641d9f91df97579e879f286b528e58aae95908f9c7bee8d5725b7948ebe3c693e7ad1dc43b3e7ca67bfca5d6cdd72c3 -e6adb2c692cdee8ccc9da336bb582f274ec348fd22a7fe67ae90effdef12d67f63fb73e4ac4fe330e111acbd55663e7e41f6265ef3c8f3b7bef996331fedf55c -1cede3777cfbbbb26df3cf9f7cf48a2faabf64d6baf96deea9a7dec5c973f415f829b75e60ddf430c78fdc77da2bc74faf4e00edcdda7a5ee3ca75f2e56f5e78 -0ffaf1ae1d72f97affa5bf78e109f2827714c0c78ebbbf7be696addaac85959df0990b1f7b83fcca8c9cb27a87da9e553f7ff251efefcdf79e206bbe70f6c543 -3a4b3d38fbd02bb5fea222379cb38353e885df946fbc4eb614deb2c8e7de79c727af5ffbdeb99d6b64fcd0aafb5f617d61ed89c2cb7ef4cad532c7ef2f7be3e3 -97aebad56caf674ebc307efeb99bee7c872fb347492a9f6798f7cf9f7cb4220e868fdc7967f732ad603bfa5ba79d45c0b0cea153db2a92320f6e3be1b18a1cf5 -f2e2ceb63c547b722cb76d53ea3469d6075fb775e7cc179f9bfda27c0eeff96324ccbef93bc959f2f9ffc24be79ef5c3c1f62b767cb4f2ba17c92c44a2d3de29 -c7fdfc9d5fe6ae5f64fefbf11bbf42f8757226ce09e6efd893b1ecf8deedee85241f4ec022e90f3f724b9185e02d9b4f93bb65f638b968d5bffdbbe0106d85ff -73dfdf7df164e79ad7a99ecc3d79cba35f24b87d3b67fbbdaf947b4f7a9c1fae688a7945ecb35e33dc67a947f76cb8e9bfeaf59f22c7dc8c646e879c62b175ee -d084e8cd47c92d5fe28733f26d958e255f3d5fbe6fc9f633f102c0d076cad58e64c76c960dabfffaaaf59fb856f5e46bb0c992535779cffb862bff7436c9b350 -665736c3b342f291ab6e9397779c6b8f3f66f5abe7e4d18dcf93357f4a2cfe996bf42ec327874863ccdf3faf7d4445404054afee7ab7c81b36defbe3afbc4b55 -e55b7ae3dfda0894da39ceb9f6edabf5953f101a09ae1abfff39b27aee85ab65eb45b47a5951f599223e56189362dd7ec67ab19e6e5d7de7f34f96bf0af8c177 -e5820f5bb2e3e1b70878fd77dfd9e2a612b28967bedabcfc02c8064e5d825fda7aef5123d920cfd543ee477ae35b1e3845b67ef81af9da470cd2f30ff24be11b -e5d693bf7fa6fc1df6fa979ff3348ef68aae69eb0f7c4ad972c6862b480adfff5ccb7af1e34ffcd667ad67ce02f0bb9073fd25ab044cfaabafa19c598e263c38 -799202fdef2d69bd46d672fa1fffcf72d36f9f0020f2fb2719aaea874e16f9b319932ab564d3acfce686df90874ebdfde5f29d578f375d20bf6d9ff2f3659f75 -22d78db7362063965c4ea5ef5dd11b34c73be1f2eaf6c888bed76e1e8fea73fc45fb1b85905ef517ab1c67ed5fd77e53e465abbf8df6a4d425affaee5f70e7c7 -3d72fba6d51444179b6823a4a17b5eb3f1b1a3c4ddfa8c637decd8ef7c8d236e4537d894f5672cd71d2f1bbeffda39193f6b56ca5ff99b9d1ffec0dd72d70f8a -24e9bf690471d460fcacc2d5231c073833ebfa96dc557fecec0767e4616bd54996dcb9435efdaf626d79fcc1218ec1fcfabcacffb2b5ea2317dd7396dcc3477c -fb44da04bd6245b7d794f8870b205b5a2a4529bc417da6e5515bc752b035e0879b5740c0cb233add143d6941cc26cddbb25a0bd42b6bf51a4942c332e64f2bd2 -469bd2b0cdd0be8529a96dbad5557bd4d6f15bda8d0fbd51bd3fdfde6eca30dfc2c74d597f76acf9cab74eba4b1c5bdcfebbaffaf0fd76f3d13fb6dc93c70906 -77c7063688b8b30fd757637ef4744a4c791bbd045e74dd787dc5fadc5bb4fcf649c5c0f8edea6c8724d929553c8562595d3973cbdfaf5dff2a3ca1d9a767e4c1 -d5e1293f1c3fb0b2fa3365f9381b3ef04e54666e5cfdc2a37f7de2d18d3ff8d7d7debd3d7ca775fa6b3bf2eedb5e4280257715408f6ad8d12602b364ed75f2bb -e30facbff4a837aece08efff2933bf3d7d87dc3b9eb1ecbf575484b561d67aefe637c8c7490ecaff7c1d87d9b577bf60a0667f45d73415530706b9a14b57d061 -d21b042486b97408ddd50eedc706d40e2a5bd56e53a83499130669b1af745d7adeb1ad14970ea1fc4e7a4a56a3be1376db3af1c86c467ad2a741129a1242268f -0cedb01ced9b203ca5fb9aaafe58ebdfec8d4e7df1cbeeeafc387cfc23c7bfe116769a3c317cfbd7df75a33c75c79fd3285b60305cfdd4ecf15857b690c666ff -a61ef4ec173ef4b2f1dfbfd030637e28bfaab1acc8c363774e7e946d93afabd3306e5ce3afb9ef99d557dab7e15f1d3d235f78c7c3a71c2d2b32936891424e55 -3eeb2e0fb6dc77927ccfcd7fd86d9df099db4af5e13f3a72cb43afb87f7576ed4fce956b92f196cf7f4156292fee7642b1edb48758abf3192eba72235bed98f1 -2aeb29b6d629f3acfa932deb81074067bfc14b7a5bd6defbded1a5b3f7df00a0f1dfae1479e8d2fb2efb9a28e2bf926b9af2b9f9fae033d75d38be73db152f68 -3ef0ac8f8efd8d37cbdb0028aebfe463175828cebac4b566e299ce8e075418dcf30f8948d773f45f593defb3af9b3d71b5b5557e0a187b32dea4ae53679b2ecc -ce57bdf5b304aa97dfb071b57cfc92f769c9cecd2fbe40367def3899f9b09cb5b2eecf34cff7405bf2735b55ac9087e5e952855274e2adb959ccc4191ddcc8a2 -846592011b608f6a837c58c91a9231d031eff41906978ccc149ac4cd879dbcdab483116e00d50a76b7598b28d884c70733caef0cea39ddc335fdb58229b029d5 -1768f24f1a0c7e17b74753d14eeed356963eaa156662e0fe403a0cf22ed6b86bda0798aee0dc14732df37a9456b2a851ef269a048c7ad546129be66cfcb737cc -839ece0ed317b72a4989668190cf8d77e4a6e51163497651c479c90ac8695afbcb7afbffca664e3dafd6885ef2ebb775de75c6cbe29d276caac5efbde27d96c3 -082179f006f9ce3ad9fca18ba8ea1eff040f47f7c599a78a3ceb84dfbfd2baeb82874efcf767eb892f67fa8fca236a5376fa261ef935b6e6cb28e39efde02d9f -7abd3dac3a1468b23ebfe6b1ef9dfb69091eb878defaf0792b11694ce91c0c3c864fe3f752a135cafb70c2eafe805214299bc172c3bc95b7d9252e1308a801ab -4f9e34acb2aca3838c927a491b4630c7badb82b868f4a0db40b7c2d2a84b87403666d04e4ab4289df4f867ac41adcb64844e66dbad2a4dd72b954aa07defa7bd -a6a33f5bcff8e2b3cfbe27ba40e65efaf5f8dc6b7e2f92639e7fcf05eb4e7d38f9d26337dfbceeebcf9b3d463e7595ac3fed2af956205fd4478e8acc81706c3b -c63a6a7cf33dffebb9e31348213e24cf3bb6423b135d5f58234fb9c9f8fbdf49e479c0ae7f7ef52deffcc62dae36ef62dd91caf6cb6e7f813f633db3edfadb1e -78cb156f92efddf3ced59d892b39c5136d1a02ef435a36d33513cbcf989586b9a5c74413e6a94efd62c5ed3edd21732f1ea8fd69b512478d88f6edeff18376b9 -cc48e28cceb3cd5eaf96166d2752274d9a79b5e156248db5cc9d816b656c7e5110e50e3bb4276f7a235a4e77aab6197f809d3f600be583bedba9e80f64942702 -6287dbddcbee995d53fdd2eb3f125ebdf6e47fbee7a71713c82b90916e3ffbcb27cbed33ce0740fce4c1b75d08730cb84363837365cbbad19f00a089f17d90e5 -9d46a45f7ec5aad9d7cad36b1fbf537610e28ee5c26be4aef2651bb1c9841aa977ebdb2845d87aae1c3bfed78f3d3694e3ce9f857e147a97e89ccc29aacf54e4 -73e395e3ea2701df7f6bfdcdebcffe1feb4e78f6f653cfb8adff91b7af7d15fcf8383e93e29ed5b463fd467dd379f4ddd8f2f56b779a41455b70a7e7e8ffb7ba -722e0c69c5a5255b608b3f7a997cf778f9264977eb01328ad675b2f63fcbbb65fd6bf59dad9bbef862f7d60b8e3d4b36511a7ffc55ce3dadbbceb87287cc7c5a -2e9c6e4dd861cb8787b5e3aa9b844e366fff78b4f6ba6b872f3ce18fefb12edb9992e0db7eecc97ee31767b697af076e5e0bf5ebc100316c963b3fba91c7bcf9 -27ab64fc069dd4748dfc1bfb50b762e6937cd6357bf70592cdcccdfd7846c6ff20cffea9dc7aebe67be59c2dcf56c5bbf939e7fcf587e4eedf199c296b0694dd -af3f75266d581f756fdeeafccdec24a53f251d3a6cf9802abff996d27066f6d4c762f79cbbb73fe78c4f5ebdf5e2d7c5ff7dede64f9ef1b7d7ffe8b527ffea2d -c78a333e7966dd6f8c8f0db7c8f898b9d1f67711c3fffddb1103d5e0ebdf3633be91dbf96794e8fbcfdb0e5783f5b1370a363d95e7ccc8e8a9adaf969b9f7fee -1f59b5d537fe01bb67b6f9890ffd8bbbd15af5c08b647c81a7ddceeebfe923b6b5ed6af7fcdf6cdc32ddaa9e83b6588bdfa007ea8801cb0955035696ba8d5c8b -6923af95d84136f4cb669aa51a9530eee541d6ee60686ba572ac1057a2e058c8d0e15a0a331ce4ac56cb5b8d41058a383f57671a075c06b412aa85a16d471dab -1c25a6a13a65e2d600273cc50f0dea5603ca1adfc100091621df61ddd19e6f3e0cffb9f03648b8b849d672a41de2f5d4f0817a3e74395871e29bc3abd6e6eaf3 -41381a6501e592cc658a9a5a5e01439ea32b63c8575ef699b99bf77a54277354a90c5a21e06294371ac3a10ca32807e37000365a6dfdca411e50c651a13c887a -668a13071ae2d62019996e37ccdd98e23a0cf9e469cfa6658d36c90f2448436a4c7d8f4929e52c843ba83c4496376c73427367c9281f3418b31357fd68611470 -c2fd56a241294d389f89b6ba2db4ca9cd52c5edc4811b396a83afdb647832a9e80aec88972a6179709362861305eb3e58c7afa7fd5587b8a6d030e553eda16a1 -900030719dc71b597ec25886d4ade4b8251dfd25a55fbd9cf8cbe9a479236967f5baa97c1fd6267363f90b901953e7f0693a3133abf2513d6b6bfb205dfd761e -75f27e1df9641464b814bc8fdcc44ce3a1df7fe8e671c946cb2657c1f662f8a5a66cf5c1e8274ec7993e54f9e88405f3dce888cd105b44540e1c8a90bc206ff8 -94aceb1c38db4f6811c6d556bacc73ed67710bd76f407189294799dc000dca50c02e383bf219fa513d0f8ae1b955a6b00cb4e7b6f6b0eb48ab4fbcc13073f665 -979f3057ae5e6d36f9b6c94155adda291dfb795c66bbedbfb7d9f237e0a1ca0765360121971a704d81f447882aa677380a9477fc7a3d74cbdc76cdcc63aa8c7a -bdb0376ab1732a03463fe82ac8be79a55b49f1bb89ab681cd0f5220a030df88a5dd6440693d5daf07cabae0e92735494281c7b1143ef779aa91f31d4c87c03a3 -63edb086452f473acd5b3b4e4d43830e513e64c51dddea6c9898d2234a2533c7cf87d8079a8f646e30f4db58014ccd40e563598499511d863301473aa9834715 -58834a95fbd551448c476300c2d0cf2b463e1df62ac18a6e60a452aa87dd7c58d6c1b35a1fa57dbc8641dc60a46c051805e0d6e9125ca8658fe7736b064939ec -7568f28900cd13e4a39ae05ac3bac421e538256b94b9f57c60d7396c4a21c193a6c2a412f61923540f3b1d9e69add6a78e50f5a6509f9ece851b9a21949c483d -1bbb52c8a71de678038cb5b2cbe53a83796bf41bebb1e790629ee9408d60103210ac5fd723ccd199692cd38e0c4baed3b2a6333ce490e493a0d23a5a476f3108 -b03fed761d3db1d31193bc1a328c5d0cf6d0a11d3fd75c8a7a4c0a8eeabd50e1302ff1cde912cff7d62ff32186678fbdcdfa36559835552c7483b4073319ed66 -d9ae63e4bbd836dd5eac2ca74709f059b39757fa5469228cf9b9cd4cc5d06accee8841cdba670f7b1d927cca52d658d9245a5c4ea8206a301739e05cef31f32d -1ed1976554b1ccd6b27b1883723ef0d862a800531e4a3ab1b2a2b33d8c7d2813e4330ca590cfc8c99b45fd419b211118b4b654239d62a9632adbb52a471a3a68 -4c57ab9937e2bc3eaae57d2f634ee33c3ac6b8beb68f345573a7e0091dbc7c386ce9578452eb5d95298af0188ad328e91046dc920a6385c5aa31fe344189d85c -445cd561a5e2e264b333ba214386e321dbc6b4d4e014d76acac25fa1bab7eb00a4356890139599364ccf5657ba9176d104c860780d23d45a8826a6c79282d9f4 -0e6c84752e27e8f4cd318f94da981f7fd4e1c8d073fef06dd0c1cb473de6505165760b76993a74b3f9553c718a516e5856b9251c2d1e43e17d0ae0013806f900 -e78602944a8ef3d36836b52394914f65904793b131bd12139be35adee66c8e14996e226a61acfc20ad71996155c7c837228a573150ccd540b4712b6e672609d9 -adb2a11a7c9d4e16b54b5ddfa1bb2deed0e19e62072f1fb85f88c70c101a50201b581682e019d36cb7cb036b94d288e9ec241a18b68759885b6cba219aa57058 -188093e6f6a859d6b1affc8c333d0f26dba08c685c7ea0f2512ba367158c847ed01daa91aab66b54f454f00ed50a79037d5dd44e2a0067fab4344fe23011b49f -57caa9eb3678026de8a0567be28d1fa2253a68f930662aea231eec40841637bc94b1e6ec747618ddb1f30ee9e40a93ba4acd5a13e8a16b3b569cb825bd65bcc3 -3a98f1b057ef76b01e396195b6c4e1c4d249614c4721de72d8349da2648970835693f45baa33c6bb9bc7ee8809c3d4cf1390b12d895fd965dd6622355cca89b3 -caf5b4796e81cbe151f6bdbc6305249c0ecb4c1fac7c181137e883a67292a69ce51db52336c2aa489d189b0d17a51e939aa4536fc604aef05d9852aa03759105 -8f364d3d7afaf8dd8ac2a488a18e45994ce8d4b93adc9993736cebefda66d632534fdbec2466ca3734418685ced45a79c855cd5937f7347ba4d369d54dc4a960 -0a74ee71b649a769a56d0999eb58b8eb87b60e563e1d69ebac5f4adb72f643acb3960317068274ebf8c15d2bd4d1424c4a6ed552c7a795b18be293b4d0de76ad -61c454423f756b5e3c994ca5392fe3ef990d8575a9a04dbc4ad507e707583bcdcae94007bc075de69fe61ef7aa3bd34bf326726af673afea5b34dc6a581d8e0c -8ba65e8154b45b5357ba1d37ad588c08d5371dea3a48f90cc48f992da97df95a98552deca3312d53eadb2d53e3c7e326296375c06ebc3ad6629860bb9bccd265 -75235a02b46a1dcfaf6346cc7d76082916f7116f78003f6a631015c3d04a52ce7849b3c49fedd81e42892aeeb3d1ce32bc054dbefa5a641f6409dd04b17b7a6a -b02b75d665575a6d5a44abaf78187d040e4e3ef4bd2285d9f2b8bd21de491535e8817d3108b055c932dab534915462553b08c2ce42869773ac98288435e87501 -8806b5b235a035825935ca27178311c8c757f9b079f9bc7603bb536b923925ea1f68cf854633d7085e63b1b4cdab48acf90a8fd976a5829a8e6c220d8a368182 -744ea4349b8ece4df594217388c1d841c847dd1dc6b34a8f995e79c2d1d9e7ff21beaa25ad0188579f69a7e836cadd648a9c9364c30cbb890d99f41149922ef2 -4acb352b6d4e1c139c437add2cacc853fde148c2001337543dc932c000bb84254afa08b2dccf876c476d74466c8fbbc061d0c2df28dc43b651c52aa98a75c48c -299456a51291a3277a3e12fb6b48bfcb162aef708965ce55dc9ad86ae1c3959b60597ab8837c829a35cbd01188281220c41efc6d2c4cf1f4aa491a640c30cfc9 -7dea4ff85765bcb06a1cca75669cf38320cc5d58d27de28736a0292e38a4068c133d5fccff324e2ffe82887ac8a79010e7436605b4d954ad5601b5a4a3958abd -dc3df030807d08f020f427717842aea43584d0e019bb5c08ff123beac4dc1e875895cb00eae1d8b2e3a1ce96ae73738d8a491deac280d41312c2b9c285c5e244 -9f2cee27095c4d44a35cdd20efd770231a018880944b6093c4250cc7d4538b03321fa9106355a928afe1331b1f958362501ad272314383d4c30a6cda9830d8d9 -066d00a43b84b57cf910745a1d4fe21e0a1cb3cbeb3cc5063d9c5c370116d6d0abcbe08688432a2ba815c254464c529f63a83a1f09751ac9ee73bcfdf6e28b66 -63d2d99f1d57860413f9e201b70dd8bdb55a37e5c8ac5034decc9bdaa99c9b05c254f970c219936c162e77051a1e2f186978481c038024f58ef1b1db8bb6f272 -45b55cf968d4a54adc31e7b1d3e5b0887365d716aa3b447b22143905b4011beee21355fd611cf08e6e1d8aeffca4c92abd92d4ce16102a8bbdbacb72464d9793 -3f19353ab8858c6fb6dad53e93bddba55639528f10e4a81aabcb95a71ce0fcd3d34e3999785dc6cbebd5d55c4fc30d638e471360bc8d06954726937208f1d872 -e503f6a55f407cadb81eb74893a8d8c0f031d732c05f1cb18d50f8a0af4d1d5b3268e2f9146501bd2a2e88da54e2f6505fba6b25da5b7561b5ea6e1665693062 -dabb1fe6b0a2ab6186fd929603aac3c60580cd3dda4f1968035f10ccbfc2b9601044e2d28645ae8970d5d531d05c106a4d4fa17814d2c7aca3dd4ccda63ba8b5 -4cf930b3c2ec703ba3a7d1807da44427df9f0c451e41230d817ec0acead88fc6d04e122691076d5058b7c9a8461af217446e27ed6bc78385ce58a141c6cc526f -ba6e8591facff5a08e7b882bd4e67910ccd2cd184ca3c1f7158e410a778abd8511efd8a32a57562e5b1821a28900b6827af31af0e13d63bf4d200de44a0f07bd -818304a697231f7c7d2c65a241a9f6094d48cec5eac002a89ac102c065a8057a45d750428e7666bb088edf68c6256e72ea232da902d7d03caca9f9887a3a519b -9021dfbb160cde96605432b7e58c70257322527b1448956123e5aee2200088fa06fc83ae862374a9ad5734f0f3b58c936c523c08fa031d00eaa86276c02653e0 -05ed4fd52f95c250f740a5d5e82c391e7349bd5a8e7cd85a3a845d5d61e51bd7889f9d069ecf282d06449725e91480858e357713b79c60b4310d64bebcdcee07 -e5ccea60a57bea73d715d21f0d270e62b81805cd5cafdc6423e4dd363b3873d1b5b85d0aad5a823661f2bc514a37df8470c5336e10d80f7ea3d2f87872cd02ac -241f64c3c242b9d56906c82bb3b7cb8257801754a890ae40039ee5ac65c807764f537701471253b3e30692d01e615e93fbd1af082422e6d02d13fb319191ef8c -485d3054a681474085445e6957304dc02075c4d5005825eb35d961e1e2fa89c4f56dcf216ee88558e25a2d152f2bd51a56b7ea86435e88548a10b582a7c5dd13 -e14725db757190d5b480a9f2fc7a118813ee1838509fa45c29ea97ca117e2a1e227847669288e04f3801cb5a07900fdfc3b6d2cf8a794c0508de579434e7282e -fa532b46e673b06669e247991737edae450455654448604baf518f4b1d3de7da5871231f4de5ccdb1c9dccbcb0fa8e26b35af9a0df031a2cb33dd3c48bec6a99 -6f704afa13f25db19de247250a5e02c476aba8446940680a4c89a13284d96ee66b8e84330205aaf6fbaa386cd284ed6d41a8e2b55d120ccb4bb21e507fe8fdad -e16fcfb26098aa883496e6df32daaf84bf91823e7a26c56daf9f97da9d52834c4fa2f8b26f53b00ce255c7b656304ff532fb4f11ac489317b11112625b586da7 -4704ae0d9221f092df413d2bf55050078045bc670f975a3b70b33aea55201f02d4018e68d3a805ecb1888ec9018fad57621777649871054ed0e58a38e5463939 -9791c9285611d8b2b0c503c967a8b79e61fe517bb60b1b0c308bdd561d6496da9008ec81d63c2a4214bfd608691febc58d124f078b69b7a4d4b4c056bb880bef -37b1b1542dcc3a7f2db806903616565d47ee02ec3420d669ab70e9a4963162514eec9e873e498fac6061023d617279047412f451f1a69a5ecd56c62587035fa1 -356d464dcb4a89b2badd2187491e11f00820869d8f958603586498f6bf0e241fd7ee776963a9dba0572a8f069d623f743c4817fa07bbca25997d06b457f5c352 -955c97a74a16711588ab2201ac0c8e3b42325e0ef08a2363b6a3f9a05d73e2b9e92e8f166dd404f288b47c296f94b36ab7c54e537589fc0174703e8907a6fb47 -99b044af04e8b0cac9fab37bf9dcd82e6543344e99e4ca2aafe36d0fec4aaf44ea1a89d0295f214bcd2e34f4991f681d403e5cb039b5b8b55ae17d1257f15fc8 -25782e38d55c78d9a409ab4ed6710909d00097e403e22933c2bb4b46bce525c07d1a7be7a35288d145113be4f87811300590df64a57697a33a61f61ca6a2de8f -750084eb565b3cf606ba966583aee47df354ba1a62b0034df66b30701b74a7c7f47a21ef490056230b0facd4c425219ee17b33dfe3d26213b52a029b62d17d5c -23f78031d9fee56342e3222d593537c243a2b4286f36f46055958a3083dc0edb390d3d02765d5a0210817ac20bc75ed69b4d70aabc865893924621045275e463 -149154df4292aae3b0bd026ddc8a5b0780241c3b0dc51fb1ce854147e035b329ca7a45182970004ff34c1045d0d996f412f5ae3c1ccb929dc420afe49ecce5e4 -5d972c6f3fb6716d41ac8c80cad0fd7992fb3fe7f7231f4e61bd5ba24b55189dd4065bb79ea5dd721912819ac918820993cc557af42ac611f3d457c332f29078 -e28c1ae0af6d675452a682469ddc5c533de9fa280f27904c673e34cb1dc00828c17ec8e13f6c2bcf817f484de0351779d33c23aa203996672555e506630c46a9 -d5f238a7ea52e5f655405c4eeac10d86efa2e9953ad76012d6a1d5a8a9bd76ba01790eb5a5a4c8b99f79edddc7fff7291fae02fc453797f90875cb311b5094c5 -f6ec7edf66c245dee7a78efeba41ee61c20b708613df6350f239e2c08682bc44388fbca08d11e3c3f6cd2ba3bc3bb9b204c19b4738705509d91e7973404b5f8a -2d3407cda7f29f05782be38968588ef2e15383a48154c2aa0ff4f61909d6933a3688cf77d4a98f6ad2455e5d5b394a7817a4a995304314a2f9693e1904e54023 -e4f7ad3f43d4c1f85d0b92d5848c5afda466197750d3bc6afecc99dd906a80ef8147af14438e159033756dc372d2f147ba4543763b09e436a0204f3402fc4066 -e800baa002825ca56a810b8012912f42774a8a8f38dc4ac466c66f66ee01308aeab1fe6748709adb5acb43226994db9d2e0946e875aa1e1e5d90c1cb2a6c2fae -1f10c81c1890040064352ce2821513a96182f6bfc1f6291fae0cf9149b4b6fa078ca230eb20e5c3fedc51791b018e9c9aeb03c594bf5bccc69cdc906a132e6f1 -a23e84147e1bd033cb6c361e7dc4fb6c7fe4d3a526c3b868e66c324099693dde80a59743442090246151053d468e701473f5192b0c0d451eea14a42dcc587d00 -cb06901673320ced98dc2cd63710a017609701ceb44574a61ad7c6e9202165ab806cc0130b8162eda4c9f1b1df1db62ff9f4e1ec20e5de623042034e8d8d7906 -4825e31032ee5d58680fc029481617c3038234a8802be2ed549c3a79843e7d8dd54344d1eb5c30140d4c6bdb5c5a4664a5d9c1416ab00eac4a356f8424bb12f0 -527832f43cee697b768ac3354516f222fde680ff0743af9362e3780fa75c53da586f222c8acfd847a5b4239e559b8c64c5e740f3f434c42c056c3e04846df3b8 -c8451efcdea2da4b3eaa25313ae2279e1ee80bd6bd208bb635f7a6a535a802d6a46d10fa16bf70f0bc9c760bf350f181a83842cb7e8707647826e05c75dd8858 -2dcdded7d5ed453e43158caa9f0a155ea69f8d906a9e0db28642856001dac7a4937991ba5f1a914e16ce74c257240de2b17ecac98d3bd493e6a8d4c485d79898 -204ff3708a9a457cbe62d1e6c8c8bc768d8cb57443a702ecd177c427d9bb3f055a427fba4e77c8479281d43752cc57dc04a9161eb0a7dfa5a068877b0d09a773 -7cf71c48b366db3ece2207705a227857987042616dd6fc91af1b33c6c30f5dc2dd1c94886cb3215bea0e0bd5b1c684532818e31e37c268a8bc1025a9f8d2687b -046245db4dbe9bff463c72427b6a5b2852010ec3af21441c967026896879bf7ef5302e796a233491ed9a282f729d8c932fa0524f3ad5c4c734a07c9869dc897d -8a684ff9645d074994945c3c7ff0ea9b3bfcb082a3e1e8b16e4228334609b5afeb1ca252b727d5b60615cdea500fbc4ec1f0ad6192dcd889fdb6a27ed034d56d -7225044c24f565d4b058a99f3ab4186da9c79779cd34ade8a8593011523365c80668183e812e2fc4b0a4ca80869ad50cdb51ad546a7415354910905f4a1010e0 -6347e2ace42928443612eb6894c467f098a5cf9db027618414eea90ed33666701f22da433ee091d0237d49223d05cc2a74930d51ca52c7a45e000b6dc22ef389 -ca74cbabd58194940184375422d9a9ee50669748b4621c1cbb31749473a1a008f41d765d133cb049a8397902ea2940ec0874db29b1c5f140f873bfac69acc0cf -4be50a8aa788864a08f974d103f552494b429e8a61f05b75a2760d80738fbeadc374688f2a08886faf965b1dbf8d6f6df26c358d8fd50153ea74a87915329efb -ada15f2c9f2c210185fe52cc175af32c6d1542a49bcb8eb322b4f0d486e8dee31b35b3c37d268ee393b651930cb0997a1e2c6490fcc2fb16b8395c499f474654 -35729d5259f1523243cc809b44173aed0010a2d870e08d208cb8049e8cdc0e1d395a3ac300bf510f0bd00452ce0a00f4ca0e7b54c10b953935845c0d8d2d82c0 -f2864327d134074f1bc49aa36f085b4b63688ca6e795ec28d52b22035da05a7584aafedc126bb17cc837a97eb4d41786bb35ff862c2d0130d80c7d55f130bca3 -ec0c2ccc509d1a388573a173057647236cf6bbd6285702735af4c85b618ccb657e1ce27d74705a478997f51ce49b928b275701a9d9e868839a010e74048ea2c5 -44e5b888d5546a7882bcbba5e57441a6fcd798eb4009e198414e28ebce18a8f98d1ca7030d4b7d54af53891ac3d81af9b05d5cafd5e5bd59bd3a34351eb9dbe8 -78e4e987916abb1ea50a77589801d26b07900f968e6a11ce8c0eac944591ad3277fbe809b128c78b332cf990a2387dd8d2ec012e1c97d857d70e948e40297207 -994d943db01cc4847fd857d2b6c9265221008c1ce33d067d1d8155d52106e631681a30d661735e974a1693bfa1b2b0cb6e886a651bb9220e2a17740313cab183 -311d1565d1728391ebb529371480ea72d9d1c02acaba03a8f83ab797fbc8583c74dc66ae31b1db645ecb42308fdb64324120f82d5805fa80f75e13fdc1fb83c1 -a35f4b6909a0d2e2d7e2cf24f876ba3fb8b71ee96106752047bc433dcc1a95b2d878298a7330be81c393b0db307252b65b6de090ca248d8c31ecf0411ad5b213 -471530e4dc89e0cc1b01795154d2d873e0a10f7c4fe2a8a3a8a3bf38bd99f338cc53f0796587b36b7aba53f0a5faec26810391696a7b40ec5bab8569a8f45e3e -2e4dea759e96820459366cc649a5a10f99ecb53a51a592622ce882ea3f5a47bde6d244aa85fde59ab63ae6a836d05ab1f8f81ea68c835bbd402ebb5df287ae02 -aeed061fcc9de8fe1e182fdbb778323d158fce19b4a4a6fe4f586b04a03690cec08a93d0f8cad85c148668a33c8c8a8414a91c0dbf489db54df251f5278a7808 -76b95fb3495ef0bcf013b9aca1c6336695635c41988e8c63a33c831f1080982999ba619c6814b72a3a618dcf4fb3ac4530ca43d27be85bb01aa0d9eb118bcb82 -61551f4d5a903ff8f23dd7bc7cda9a30d26b47a20081bb56c6d3c6e934fb23f78256c92b78c503dc7bac55ace702564ffb03f0b601a27379307d1f37a78ad394 -3870e20b1f96f981059831c2b8a150f8d09d5ca1229631ba88578f875ab54f9a90978166d9b59a06737c2af5bd24536190eb1d53b8d203cf4c422618190dd44c -33e5e04514a44794d576eb95c2e4e431bc9aa4cc654356e84357543e5057da9a4389ad7a8a3361e24cf0fdbd5dc5897cba4a9831b2df13b606c860628e82a35c -79059c97f29162c121e06173c775d58a50add788b9894a0e8aeab86ab8f6d00d4850f1b434ae20ec991ce86c058e163e145c3a0ed5de60a121c3e38ef0e17d27 -2da1a011fc4be5ca70e6f5712fa09a69039c96d19f4e89b34c7320d900f059454c665a3f452f921638a80443579b604324f3952a85b7d62f917b81ab50a90c7c -828b56a547e086670481132f819d4a98087da7f0421756219fc1442c38a28b0cb3fea68d235bae67f8f8ba6b394b16b65ea20c13f5dcbae63dbe8e9651f62882 -0481caa433d420c7ede1bbf81c3a7d8b6444ac5fdd4a0c67ac1a28b3b7d10576d79f0643f2136aa135cd0e2682f3c947730ce3346a6e36ad912ad41a6e7d4564 -d217c40b244d8a065c1c057a16a977a2c94993696186a8e338954e92d6bd124fa851a2d241ea15ababdbbde20163711e6ad239a8a4cac247e114becacc062996 -910fd3d98dcc08db768f47511d38ec3c32bc1560f9b467ea860af9b6f419f3ad7d53ed56259cd10a9e16664573a975afcae9eeb4021e781bd72381c2aa45826a -99705df993b2acdc0c2554f056073bc28f564e218771cb107df44c87ec41215c500a18f099957a2a0905ef2953e0d1298f1c10af4a2da70ad818422e8a9899fb -54e62af75e69ab231b927b02aa6eb26f9b154ae6bb841a1c277a0c35a4c5e9ce35851eb11e6f92ea003ee36ef2a9958c948a32d0450be0b296a7f83803bb7096 -f5f76cf1267764ae955b1b1a73a49e04e155acc7d4002d0a1cb7068703361707bf0a9db30780823544b78bf40c66b23e885d635694ea44854ed3f0a6866eca6e -c19a30c34a060e54a2811712ba963484c77a7105789249a7db889c6198874dfa98b07ab4a15072c750f324641400e1c99e2edc510bc7be5d1ea430a77d1fae9b -7ab9285084ae0f5c45eed96b25ea440105f5909d2cd59f502f994534b89b78f84a7e406e77c2316d4eea2203b44619a47ada24187dbc22ddfdde20b1b0bc1df6 -6ab3ecf899e7b6f832db44b2d0c412274d071c247a2f99062138ccb0e0bdb0aff2c157c4a1ea1517e2a6d42fa13f144f7a243880573d35fa76cb3c5875547ca2 -2b6205987cb61d3258d7b4bc6bf2459a8a84484c2a92d8832b81435483d6a78bd0cb50b6b537c300283647faa8be7e328a4ad92a7e82d63cb2d4c6163aa4f2f1 -8a3393ccf51e916cc68e1f128701e4b100cccd69c981660651e7ccca2b7ceabea286f50135cc7a728283549c2acaa865166c358d0795ccd924bef28b6793018c -642991936464f3757f52173724f1aed7458a7308facab03e8a9f3aa4b738dd7b35aecdae78590c90d4e6cb38dcb1e5d588b34ab3a445c0a31174cd4af0161406 -8a0013a9b8cb47b576ababc164c2831d1980134f083b8902a15d2482d1799e25a133026a202a0a66e7131bc867a07ea62944de1329c2ea288857c0e13d45e8d5 -05521334e02f092e454b0d65aac94a5230554d3d0325d4ec4a803febf17556292117430e8c77289f000e07c10047b27117d2612fa56eb7cac5c04aa3df91e680 -6090373b70581b005825bf4339940ccb6489791e3abd58b586eddad3dd830de310d0920b6d2fd0c1cd51a5b634f64523888121a05950186beda65bc721d03341 -fd762d3bca5018b06876b3ce1d49e0fc412ec54c59010006c29a602fc82755a74ae15873d02e5a00dee82bbeb99e197ac6aaed31213cf099292de9c3df36755a -ec815108a41393f76ff9658e039f27ca436ce5fe8008a9e88934f175b4be38d20c1f46497fa4fe163b8dd39e79e8dc590c55b59a4234aa96ddb6db2283adf944 -bd30ea27015fd922d5beb27e20ea2b8491f66ba62b8781c134b58d32f6502c4a9ff0ec496e56ec6e14d57d7cd1c052aa00ba5bea9174a2304df77a45fbe7b0ba -16870c1b123bdde406cda7cdfb879074f7144f8adaa45a4daea74dbf207a650aa8128af0341b049866b7288cdcb11a2dfe1fa25ca3001435e82bb18c21efad8e -4db8822a4c0209f5238dc9c483d5b9c1bad4cab5aab8b945e960a78931aa8e00a8914f577ad0e727ee071e40484caba1778ccfaec749b3841301999518901632 -66570ca4db18b62cd053220efe9354350f838730cc3a4d8f77a9bdab5a29658e969e130989082f50d6104a0750612087e604789fc887506f8f93bd00064d7a42 -676d0220ab4015214c8dcd497593a8ae825500487546304b238c751280c8d75a194f8f5e0aa5910d898c429be2d3f904bfe842361c0ea920299e1b001ab89afe -c9d4a4f6bd1a086997760158a501b30ca46f60392e04f938a61eaa03cd7a8004146d43d1fc619d976918457e8c928c006709d71a97cb4c0837ec43b511e93068 -83e5f39ad46af70592893edf529276a0b8aafba1c0a99ef12552531a144cf427728c5ddf6dc1e4222f6bb4666111807304e8d1a5dd0d6afae91a849130afe7dd -542bd9e2a0ef86f873101afa8ed368e00a3a50e1012f8cba86505f544c85433a7ff6a275e06c83660e7ccc6a3781959b6a71b272dd4135463c2a7d7b211f85c8 -1ad452395cb5f2d140943b01eeb7d6e9f20f181b3a95724e286616992f64d74c3c5b48fd75a5dae388a7d2a36da52199eaeaf8361b46f38acaf957d283be13fd -d1ab5d005317441119976097a3a4bfd032e308ec556d6cc304ae248209ad03003135296d765dbd410ed3f0d9f53f43a7cb19d5d4b4b84a283621a07a1a2a9ef9 -c2380d3d4035c03a0af944d4e0a64d463765387a5658a9b48350e9740672e59910c776ba5dea1c38719bf037e1d9681d7382e130dfc9a901eee1c082e2ecd5f3 -54497d18744e1a3d4d62e3e70162d7838c3f40579c5f616075424a39f53cd49a752e52f7570f2f45157bd7e24e14a3d87d6176a018912b2e5662b29ab0e1321a -0154c09e474e4b7b3cc0db2960c31299dd88726e007b18c93825bc595d033628b1294b9b909835302c444207dfa8191c4b7e96e231a71e104fb9c201a0c1763e -e4ed4479d546de0260a70c577ac3764f4fb0b29640a996e0b4f3fc860877e4e12572ef4d1ea361dc91da348f9ba23a979f556ba1cdc9412100f95ab3d1f9edc0 -a69b5c3586122c032688a62a1ff08049f0be4840f3b673e147243a95ab6c6c8f59a66a74846b8f17a88b323988089ab6004f178764770f7f1deb43b11af895ee -5f103dae21510dd43f4c3283fc05171d4089848cd9711535e62934b104d88aa848c14ce3c2c61431f385808ead26ec0ce974ecc8522a7ad4a9537869e2235242 -b07ed557a9d1c1413d5acf33541c656e90d134fc49cd21d4ec5409793809369b7121ec0c2c30271d4c2a1c0103e4c367ed19b36377f66ed000b6ad59eee2d670 -9971691ac0ffb50c5202e2190632d221c91dcf8617e174b5d29b3167a9474f478d0d798238ccba0cf942d7bc7c38d3d41b429d34e4447ff4af0821c8b488dcea -7a6462acbee625b4da0c41f2d5837e33a969b52639268c67e468d309b67c29c15141361c376929880d895ee51ba9642a4ae46293ea23d55e65a54a0d0192e0f6 -f89939fb143e5512891e620eb811f289607aef6e66948646fe73f755c58d4ad4d819e928b0c4ce267ddd67d77baed65c9a9a0217624900fbd101ac2835836a26 -fd1a0f27213ac47599178f29a81c72d04ddcd4a2ae8b78cce043ad009080563f001764faad26c92c8e438e29e302c0b4f2e1fe0ce33a9e3629b4a6032c4121b5 -f974306f9c33da98e8dee809a5f7e81d4cbe08b603f11c063dd4c4ab0733b2e581225350d305d5248e8c14ecd3150ffabc42f33ebaabe6fd9fc5c2407b76038a -f4669a98ba8c24a759d51ac1c3504691a1a9e178b844f671a8a52844c23cf38116cdd9d2b521c9415334859091522fb8e3f90deafb6d9a6a4cbed7d0324c629d -2f2321036803deaca6c69741dd8a879c5461a671e8443eecc47a346ac42915792411e3b680a1f0e61e274f8d7f3b7a4681dbf19c741167bb8324230da57e0cc6 -bc0c9c2bed1e4d9d141ae2ad38f4f3f94afd1c8010c326584a3edad569b7454a1215355aa823ca1525e9c460833c373ec7f76dfe47e513a5ea9c2538aecc3f4b -7834da8887f235d50d2e00778c5580451c24b834c33aa525c5aa140e32d5f3fc3fa80e88021a318a5daff785fad526c07bcf195195a8f2c9fd4cfb30815ddb2e -074024be5721d42b8e181295e8b156c593ac959ee65a3191d4d5bb090f817d03f5453847e8db51d1c009f844716dadb62173a589fe62370d515d6e7a49fdd95d -3afc4d111ced3ca28fd89c4d104b489897d210a7b55322375cd7898cda6e1eefaadbb013608f0e866024b4d4d0a6079163ac8e8667ba6287cd33ac9bc686ba28 -48d1eb3244a838a0b6524f1510fa76991257e1a81a1063e55dde0c173e2be33168462c4200906c7a25883ec0f85c23b7d617a7ea10e69440a2e06a6bd56a1e8c -721904e45fb097f5a4e9424e246598c0d7021e0809fb079a36e21c9c2469cd35f1e8db4bebcf9ef2211daa1c28d0bfae39bb4b760c0349b1169f5a5d4d9d9b2a -34c016e58f55213e56fd18ba3d83ca695f83ead8a11619b348ef988763fca06199960a859d0b0bfd5144407359f0c434cc0075a694ac014422600fc55ea43712 -232db54a41f3219e9f91fbe9da366529f3c70cfd2e14d30b0de65da5bd808abe1a28559f1490a64d06e5aa02333d8b87de53364882fb4011c9eedb261bb9fd65 -c9c7845b9a8e24c706ac2bb55a7560c54c6ad7ae4809794df5ca707b4af021f1e71b64310749c3ae716e490f87bfc746d618851b2b4e4a75b459dd326976f54c -f0438b3c86769b00ea76b53ac704da14a8546c9b3770f9452514f016407e576f26b3f1517a06fd5697d7d52ca2bea446eb5787f0460f2b2bc1f9d45469420d1a -f70028dfaf0c4855357dde0e12634afd2a984b6801e652765f4bda9f452fe1f515b53db0b15a585f87d38a8342b73fc645019f6060dc08ba819532f88a7427c1 -a7c5e3f30109a281ade06cf1cd2a1ea302593221f783e67b9aa8503914a7072088ba01347aa9f4a9d4653fd6341b813f92245a86a337cfb32a530ea34e03413f -6d50544f0841c9f51407a4160ff6949be701c2e003e8721a1d95b2019f820e4f362ec5f0676232fda447baec84cad22da70e241f05de137c3575b9313b88889d -4a5a2175f1851d89ebd0fe405f9c36c4779ad1d4d20ea74ed6c4e88027457e8d2279255599cb9ea42f324d5f68e892f8008a4a48e0af5ad38ad7bee07d56fa91 -91559f8842da91e03f14a613324c5e4b0716cd86f034fb2449951d6ba834033248c5a2cc11e74129d031fc2155c98c3042c16275a1a9e284df3e0233269ea4bb -450a4061ca1e965807924f8ca212e02a525a8847d1091bf014e35b95a8a51c1b757b88780b0e66331bd64b18ee80e6879d92463ff363ec15c6d035ef25f0d8b2 -6e3a69b355d46541ce344bcb23b4dd8b56306127dbd4b83aa46855f9ebbcb0960ead96e1677964bdb5c8aaa6d7d719004d1b2192e1a10d81093d5160d28c23d2 -0454a5e520036054a4bfc8c98e74bff5384fabca48d47d7ef0f2892995697a6d7563798c23e4a0c7015fc7414e97842ec1b33a5274af4953359910943816a1f6 -68493fc72ce17bdbf047f49a0b8009735bac9105ab6a40f70cfd6d91392cd8e230ec70dfe83451f44884f7ed3b96139be0c0c8a7920d2a0dad7ecb4b8394ca26 -68e302f0055d711e85c09a87182c87af867cd7ae652429b1fa2d2d4d0570c6191cc0bee7e171508c90a2ad5f76d0f219964ba3c4f6076a605cf52bdac04a44d2 -a88ae90569ca27a11383c365a94db511658215c590e1f268c650e20a3ce6827a931b4ea752ca277e2a008edbc36f348251ba944a864dc3ceeb0664f1d95fb4da -300736a3acd9136024bc02081f0476447ca19e1ece2fb598568f8922b0ec3887e6413ee807383d3da889600df4a4e2c8f30b03d45304a0911191ab7146095134 -cee025b5671ffea1792d1f8c06d45cba60fab80d16ad43aa1c1738ed459b67c4435d11de8433f4fc34a3628e1b3145d43dcf282b6e6c6cc31b708de268328e85 -c928ae44f1da4a7b08a1d29c4ba635224234ee91a7ee38edc8cc718f6fe241aa8413eba80c5ba690a7dbe9b76c5536002a69a1a535821c857e0a73cfea96bb28 -8c627c0a675b49c2aec2171a8df82920249a16d98976aa6c5b2681a3766d8965f01f829d250a5731a87edba220cfa1fece86b856cd7a36c55cda2f0cb62ef0c6 -88ef24fdab9939be9a165b462e5df02bfd484c15c835b2d5fda5d6c2dcfa24c0ede9a36cc06037c8abb124ba183fc4d7d23b81ca1b3266939c31cfbdc7853ac6 -626bfb76e2af7ebfa9612c85322d108236d38c2d4536b452a358aeedd89374b4c6bd1e1eb99ea556b9d20b7a117fc473065048b5ed671b03b43ffbacf7b287f0 -4630d57c202f94973a0a9aaef17b85e731177c5caa144d0628f1b306239b50722d0335c7a7d94549c3a118878a9bd023eed0f0adf8398584c52e33af85e74d1c -a59e8c125b26f231c3631413ac45f39b85e834c0dae0cf61c638b3c9c847eda802a767a0c087db0a6df2fd2c8a7b8b640cff6a3921febb2ad40835862f4c2633 -a8a6f50a193da89043ad37e28980aaa8cfa8b25f6215e717e1c81e1bb0a8e4739a398846c50db58646ed077b5aa16203db8fcca6ed282b93383b28158eaa5e1d -0d68a970d7226db695adad78d163bd6aedf962be6fd293c789804934a9335999a63246fcebc246c08cceffbcd430a0b1967e11c2e3028528731dfcd0d466a043 -4e5d2be4145e681567188bed5ec9b5185c3505848c66339000b2d2a8930055e636156cf04c25541cd430d49632d093f37d4ff3a49e5e5f69355c28847780d581 -a1af601f426c0f757c38b46a2b9a9aa721adacbd2ee63fbf6b5022a28a7623042783713fc1dfd222d59ef5f1f5783160453575271581c53d69af2d5048053d27 -6d36f90a28efbd2aa9e77c38ca32be8e22e63604a130a042870e4c21a4b3ae900863718d1343c787e18d1873a65ddf30e9201084375ed0a6ec1089d0054499c9 -54b9125b4c32c80b0f6ae10ffbf07f4cdb46529700fc4ab4aae27070617a5fa67308763351ac3841c194b3045db7488fb1466d68a8fcaa26e5eaa8d9a43e279d -24db329c057d1159f3c9f7b3635a60a793bc246fa7ee7f346a54538edc0ef7aa1f38c43fa54924db41598774efd77680c30cca268c295038ce4ac2673bd01a54 -a5eda64a79360b0682c10c386b49042372700d5870f4f1a086048c483b534aa4cd0e88c7f6b196960ff012dd09f0f9514f834b446a7a4da847a2408d49aaad86 -87d0f779dee8a68960cdeac1b1e3d2aa7605af7a44b790ce02ce3cdfb757d96093e568b39c6ad1706cb28251a5479dfc7c3c6128aec85a89e10a916929178aac -e50ad5368455e4072d9bd6094509aa22d70b3d19941e034588bd4a2eb5628e29423bb63b917d4719fce4aad587d472bb25bd9f7d9def09ad3cb47a4a1d7f73c6 -92e9ca34bba515d62335a8848013a7b389180d815b17a43fbd07da3a526484596ff4b4c55df174fa93238c86622ac106ae80e6e19dd1421e437f4e1acd49e166 -2a6f4f17595af52d914fa662a4300c8f9bb30db46568d1bfb4a331080197e3682712435931592db3a04ec20c8aa0466076f40c351d38013170d415dd05a143db -4c3c185717acddfcbbf5fffb8c2f4c491271a3c93c55f4cf965e1f2c6ddd5814234197328d6434169e6053184763a33dc364d2d2b83e391fc5a5588651cb2a92 -ca80c2e689593d5c3c7732f9447f52eb11c473bcb0ef7481f0a9d02807f1e919892b5c5715d09ee3d01360cf686f4e1bdf3d800f6a6a4109a613cb5865be468b -40f116289e46a8ca1950ff4ab354204c04a9893daa2b1f56ef4eab809758fb948f8a1ecb61aad74c1655592d5a71458edfb4c1884de26880c9a52b5b71a4662d -fd8e96a50f49c3d686a31e1e86567fedfb4ac2a22bbd5e453a9fc3c0a439b109db8b9561f3da393b7672a0d2c58ca5bd4ff848fee59b110c26b645af2d32a454 -d86bfebbae3c573dc154603cb979a605ec06686a768ab3a971858a46ad03b52e58ec3ca81950483d7a2da03828f99817374c67d49e66494c232360081a671917 -9d126b7ed757f1cc6f2eda7faac936c92d3a8448bbdee0585533c8a2ab927e56aaed71089c0b66a94ad36f35b35d5d43683b62c7e449f5865845b5bb99ba57ac -4ea425578d51d662a703c101fb70868e28cce581b02072a0a610728a4fef5bf8b31c8aece90e237e94dc8fec0938329d2960c75c9ff97202a6a50dd0fee3f758 -c305e35a9207e78f009234331c2a8b92e4326003510c6d7f0a6d3648e6c898ec0e0108b5c3355a101a74979f4c4e08137a640c3a308b3e5299c2d73dc4a0a101 -716a082a48d251dbe1b094cf6ae4e3532e58ac3a57432c03eb6e026b502c6085d0218a2e37888c2b35c475fd3c12df203fe49932fc64650f4f383b9abd0b5a55 -76177199a6ec97dc5dfb8bbfe61f70a60ee0840284336fa3215caab2120ab3acbe902e2ab6f01fd586871ed53a3466248502f9896f8643aa1689df9bf2632d30 -36ab6fc2513d16954aa49f93387ec97824b0cf59f3cdff904f8783dd3c076879744884de477d7747c54164c3e8317a2c1bf140eae3d395c435bfd87e3c3acc50 -c4d30cc99fb19d952f31f40891d11cee01f769e1e5bbffe140f80fd904b55e1c417045e9274fc405d85d34f96ca8bba71c6c5de469d92c356ecb54bb0318b51d -78f6ea84812628959605574b3f8cffaa59d36a755dc464d81cedccabcd3e84b67e6131e84229d366a97cf0138c95e372420014a2bf5a0bba20715746f22276ec -e2806f4aa8958ad1423d055c47b6a29a04453f08f0948fa6c7afd7d5be8c7ab164a6f6219e03e3cfcad73575825afaa6809b66424d22d8e8426fc27e50966246 -96c6a4724c15b71fd6d883319bb3da02fbd57b8bcdd826e5239a9bc6c3377f600380a31a53a38dbfa16b6b6918bf6364c1bc7c2c7c9f5e0c8b41bb6bb6465ab0 -41c5555d15483bae6a2f48431db4aa1c512a771072f35e45267158716971344d4c04084e910de080f66e802aa219687de192223a80fe8425fd9ab0a48f8e47c0 -41663007adfac24e68c6bb504c6dda6698e10843e30d18b416659174a4b57894457f70b44e37397945f317fd6d116528ed87e3976738f0397fc83ad26b535fa2 -746673e524482020e15c231f3e0bcb1394cbf472f7c340e747103f42a4b2b5db9949a4349459d254d8dcd48469e356945e93030418081864434b8fd88386896e -ca78f6b1f62f1fb22a1a45e003abc712910548345688a1b364c564b8629bd314a9c861856aac1008e6506c6d3ec1e6d78250b3086cb5a144a1cbda4b61b2a075 -010b6abea24f2370700aba681871aad69ac78a7cd894036deba3575247b388eb80150790794a58205a94e9e823e3417349e423946ac3cbf569e04d81ad51258e -7f86a450376eaacde459a505f271fb6b127000f998c75736ee89d7d6285eabd50d88a3a4c38c9e2466c135c51352560c374dc653f3a45221c3c1a1ca30cb623f -0dcdff3576d355e8145d3838d171a087018a910c344f43e3a58244c91933517ac2656c85ea81ae41977cf50874c92d53cda9877ade80208578b420b703312fa1 -a6c5d045f4c4258281a26172fb78ab58664511d220d47eaeeafa9037dcf73a907dd6cb34c69d199db0d80db15bf72ded5527cd57c86ca95ba88ea2692061889c -0da5ed83abe2ed94272c3a63138d77a9cb8ce0294a37d00aa41e2013f83e266f635a44164504c5423e1e06d7cb3a6adda8c3c041763da2af402d56db6f87e46f -8df1d1b9137c824152e1ec2bf1812a1830433dbcb45d23ce9032ff6b756da8ab7c3843a0dae7da9f7ce2b6b1b58c2a514750ab5bb5b501d02161b436a9ec82e2 -104a9b3adb5019640a49284095daf40b73b1ec04dda3796a0d9de9e82ea33576aa020500d4514a35d033b62c50fb0cbdd6c0d326c6ed1551826e35e4d3856ad1 -0915e5a00c55c98f90d24a258b66d4526e4b1c98d38b24025aa4f6bfabd687322f669650fca84c4a1a2b521ea9ec16f37c42e4659e36ed020e553eaa0ef30f90 -943eb7a46950ed5e406b420e8d022f4c2153a78aa9d6d8d0da81de948395cac449b8f4249dcc31053b5e31bdc212c605ce3c325e110808f1bad65d2b3f5b5fa6 -58c6fc82e0adb37e9c9eb60e370342d4aaf0059474d198b7aa64bea8dce8743d8a06a461d1bbd7e0dd5515b4691785012226e3abda8843bd4b168e285b4d4b23 -eaa6dbe63ed7fef797d11f3e0d0c48fb0a9967afc7f340ed1fee9a994e0529cee416fa86e301eeadf68ed26ceec7427d52ed7c59485619c3452c66241b16647a -ed9340925ad1e309ce6bbca589c3acbd12603353e7846d2d4fea0d5ba6fc98eeedf58a36b1680e028a306864017d0566a8091d133d07a2a68f6b4e0f13c5b272 -f0fd74526f66c848aaeed121f697582c505215b49e9b7cb2018168200acca58ae0257a6c693f6773b76a5ed436c255d4943355da86f46d3051cccc441f8b329d -02ab576951ea0d4b9ebf613ef854e3f14ff85c4552b1590d1b199fdf6a5575830d3cc50f5cc0b55ab7d9b761d394bb54e245d07b7c0f3ab061a9a4b00b0a9453 -f34b84459485ef82efb178a141fb0a52f93ed732ec33e6d21a29626b961606f4557d5a541564d85f4e515378ad71866e6875b6b0b34c1148ad7008495d378b81 -da4dd727bddb029f2928cfa828dedac857c234997c45aa227e66706c52d013f7aa919532dcabd0876d40f8ee2b0a00f08fca964736b13729f59810cfb13aea1e -68ead8786dc807a20c4f658827af6df0b5ab81d9a03c54cde3929059e835b8a48c96259fe29dfab1f5b2b60d28f7f163d433047d80ae9669e0ae0f3dc1db3274 -0a9ae673bbcd92720db5797caa6885bec358a2822c6bea7d8de9e43c8c7d8ab9b59e6942eb9fbf56d3bb845bad02b077387d6a2d3fa9d06a30c41327e64e9d51 -791404f846802d890d8bcce79b99b36bb67483ff72b868b888bfdfd5ecc6245e510c69a4d08d3e63e5bcef732d533e132584fd836930708dfa2f90a0a90b3068 -100a80dc88123410aed640ce41d5b529b171e0351a98783c289aead27ca2502d5186e7e6a53a92409b454d96f9c6790627ace76e90f191dd6a0b922c938c3920 -74080b9d03a893a44ba9ca197ba20dde4b4a45d2cbd1d011a00d3551004bb3746413f5e7995e94ba015a1da9ccb1c396cfe403baa64042270cb58b204a272daa -c76fe23e6ca069ab0781362488ee4b5a2f9b0690da91b400d9f4a24dfb7dd310115d32ee103638485044e433718f0a2662a619532328d2fb564a8c97b97d4e2d -802dca9d719a19ec34f2e01df914e391aa24006af468594369b5c1dfb4da3853e60cd8de9020822e16c50236d14c5582faccd3e7f721a265eacfe277773911e1 -cd991f29ad405b7dd6918016196a1d7cb304540d1ce7972100e8d961742b2c510ba91229fa7a987a5f2ebcad3fa40f423024a882bf387fb29b49838455c51767 -9c911eed4aa19953146d6bf5317fd619294e0bc787216c664b05d4b244bd8156862525731630434acf02a80d900be70f77a3f0ca6e07f3a390671f92297e7cb0 -f2c1646afec41ca105944bd9840996631e8672b1e8ec807358c28da41764ac295693ce010ad21b9d205e060fe20c2c126274d3a8d53948d831068b0578339f5f -3498d2853a3585de074ac5857514311e45932634fbd591b2acba7e2d4c17af57e9d2966e123482c9d08d4c41322d1d06962c16ce9aee470d4e4d9fb9fdac8395 -8ff2a5d48f3326b6adc13da5a0b1d2260082280763a70147d5496c719cb80cbbe01706d89968c224d364d0a2099f0e3a0b315109700fa7692291b2261bb5007e -72e95d88b8b4dc668b8e7a34abc9a4828f00c9a6056382ff61938ddbead2a1a9425f02cdec26045bb493538bac212917a25dba74a92fa4302ff6bb7fa0d11807 -291fe5aec14c9f44bc1a32689f6abac49b7dacee1f82c036706497e0ff0ec97518f750612096093cb9e7e2ec626315ca8def4f071d22490a6a8b5b3091cd84b3 -a0aff7215032d3c844bed4e9e01da70d4067fa753b1ec17ddb963e1c33ba07a594049379d7dd99f2e9a9928668cb051bd9601cba6228249ae789900d4da6f6bf -0e523e3c167a1d670bb300d8218431e668d7d1681ac113f1d00084aa8224280ff9091b01a52a42ef589d612ed8e4ec770118a422aa0e9021c562857c68ceb0db -4a334e1fe86650354c5e9de8bdddb4a026c17f9436f4129d91805f0afac2a6a38f84514f3cc496d601105328b0a105e5fa70c04213cd21601c487b4c5b3e7b7c -1ede336900f365ea83cdc7e79083287ee8726e34f4cc8827294d9dc5c8ea4d10c4f9d929fe40db31d174d5e99b22579371d943423c7b6d388a02e145961329d7 -246a34ab242069bfa52d74861af43400a28586c645af1508917c9c560ea84f5f508cd8cde433b928f44dfb1aad8c7ce6ed420c3d41392b7d9e8ffa5923930768 -907c579e2fbea271238b039e55b07c4d2daafecdc0adbaf9466db041edd96858980bf8fd1ed78ef9a116b545a5206589d89c51ec947987d0e630a1d1b3a21824 -937ceda339195550537dc3106a7d4eb2a8d9aad629421d5ba8d5dab7900e7a7fedfe5140e1ea346be8a7cde9b44e40493ee45a1c4aa1b486078de1b82f043159 -f418314be369452b62ec66442929b750365c857d0caad0a400aad121d3459350987d7a74d528422615aea5b6e0d86c6c92ee8a7968ce38292b45a3513cc2229c -314b5325b14f0fb1bd2a73f612d4e1c947cf32d50d9d6632e4ce8c0306664974ca100322fdd4745730dd9a772d3a0899a5bc24b4bff0946de40396cc07b2e6d5 -738fcb6570448dfa371a3f6a895989e278e030a76e8784568ed3521f11ff1d5bcd0ed4674283c5668571089a3082c531ffa9a1a90aa35a71de5ddc4b2abb7e70 -58f249399ea2c51de41550e729aa7e27d5b2dfcaa03cd3d07657f5995118ca95540689810ab58855dd478876a6734f71c6ec63410aedd57c3a2e822748a5af3d -3678121ed572f81a25e683ab7b41076a726f0951903a57656d06883dd3413f130169a8483ba6defe919fe2120e463e4b3e554d66edb682841b2f5732baa198a4 -659bfe7bbbbfc06c2b345c1bc0e82bcc5f28515256fcbe78c8c55bdcc1b006784661198a43aac9a44ccb10c0303d508f75030ee92b516107f694148e07ca9805 -03a44c08a07c48a2a73f5c8ec5adddf7f9440e463e4b7d88a14c2d5e74ce600075b94f1215e69b6915b228778b8893824e3fbf50752377a7d71f2452359386f7 -b946310ded68f904af409a764665bcd6a5d04581442bad5a699ca7c6bf5c22bee1c032fd03c34a0fcfd2f0f2ccc265e4c791c4bb0d665b29f9249accddb5cc04 -8cbcd32717d468c37d27fd6f1ee5e2d5593adb542b830670084d10d87d5c706308e6ac8e23cd4f71b89b51465548b7ee405143f9fa7d7672abdc10e628e909af -d91bcd5ce3e528f03811909ea890020e10784d5e7d98faa3dd079581687ab0e8a229288df719c0674366c323437df6d008a6892cb551e960558ec9984d5ca4a5 -e4c3db6a3e5db84723a217a105076cbb8c9a878066ac38c410d113d811a0f0a6104241ca76472b8214089bf01df858d52ababc9983757fba5afcee30e54336d5 -d4fe6a364c17a185ce79a3715313e70f0ca3522a92200b2bde07e01b0983000072e6a7c32f7de943348c67ef5219676ba68402374e300294cc8a3915685ba3ed -ae60aee968352e845e8e661ece2251285f0f5c6fff07c1c2b71fa67cf4738afb9f787e104495f262d1e05323ca78af96679d7da59b0859cb7da0a5c27734a9e8 -bd16a077bf5f1c4cf4d624b1c79b1c8fbe78f420a05a92ca756446e9b4d06f14601501e112285f71f715627d0e10b7cfbf610af2997c94a6c99848aed80ade73 -056091d132b5bd8c8d56822cb9869237061aef9b652afa975894d58fe8acaaa9337a60122144646aa12c528e12c26085174a6a49b9ea6a18957a84cbb157b042 -99ab265d96b30e5f3ef39b189c3945933593c190e82e34e0328661cfdb5cd45b71cfcb7387e5b2c6d6baa8ca595a7f7ae46874c7a29cf884dc27f477126074b4 -711948ab738d6997e49b642aafe2140fa16feeb1b40c6a2f99ed4358872f9ff90fe61b21f07668ec0ef7d181059768bde59ed741f7ec7d3f36d81050e24d52ac -28715972a505b3a14c7a47a8b2d59e740d18df3a0889789448b452707df573f0c38a41128b17c41cb2cccb30cebc697af2d1240ad43ebd90aedba3b089cc5c51 -b4bf6b996bdad785f15029e5285a97ec6f4d6c9a0e12d67ee674e485160e3ae675408286b8474dd8beea57eb874c80cec59f37a4647992fa3db090a6271fa053 -aace4d87c3ac3dd216f93d43f559de732ae4c6b8957432187e9f12820e615cd218a00eb65f281443d179c4827da3edc03907fb1268b54ae1f1ec418de74b005d -e6dbea1ff8daa6251fbe891093e37d48bd5eaf06679dde3a15eb00aab0d7af6938b5ecb7806ae8f0187c669da152637a51af55765b752f66e215f647bd67f274 -8655bb58858766f2c232d7b4e483cb4ea84e290ed4649e6c87ea3c4f4de841ae8592a6fdbeaf78ec74e4d7109c004b7ba7c5326cd2a4bde2f6b5773118bdf616 -dbc76ad01669996b6af28981720d2f5a9d5c450e82be69a074b06bd957ce394e0c4ee800764b0d1ff93f7aa1d05c958211ca2ab5dbd61e91e1ae2b59446e3dd0 -e54d4d3e7dd8ff139a37c8858eac37d99f155bc8b1c5ee3225dc3a86a64636a0453307b2dd4e427a439964c6a6ed2df049f66559973635f9f06dca623357a495 -de6de85d0ca85ed6451ce28b183684034ace501b50583a9cb8ac696d227847142ad9d7eea61fe6b2b5749af2518244b100487d0893545b1ce2ad2fef6da45141 -4b28c16a115bd0d19fce671a0dd77caafd81c8e6d3817b7c98f69558f69aa27c76e5da12729525ba332dfb2a0ef585783d0c6e447b52a05930e898740f734384 -293cdaca606903d43c987b3e98d71ee02eaa0b959f40acf4bd9c341439d47b5fcefbd8263aac2487efc88cd604deb8962393be74b50dd1d21d35c8bd2e7b774d -d57f2e32ecac98cbf55bfe413b3fcb91c85eafd1264d5a4203a268f693696eef1299360d86b8f7322db796bda6a83fc5d05e3a0ffa410cff60ff737f967d8107 -7c21bd91283297b86f78b6861e41a3122175b257bb66f359e4150ff899bb5e3045f9980f05a0d2e6e8547599c6564760458604c35166221b837d51d7c2e1ded4 -6addbd563acf9959dea5fd27c3c99fd21a8b3cde3bea836bd9f9f239ebdc09e17f4a1fbeaf8f717bb31b45be30b7bea8be9073d78b9cf49ddadfca1be58987f7 -7cd35866e5fa83baa0e589f1205e45b99b3688327572476699de860b23b9fbc4396d062dc08258120a5876e8652e7eaafaa30f66ddbdefe1a8754f1a5f76508f -e9705eecaee2ddab574f3ee212fb3352bee79764b3f57cf9d2121fab2f5efe9aba7cfed2be42e496d7fd8337b3fc8b98c62bd9db93f5d687c57dd68f9d4fcbeb -e4feb9c3fde869cb67fcc92faf96ebe6d66c3a4f765df1e15ee472de5f1437e9bafed3d8a17f24483d4fe4bb877b15d3968fb5060ef03f3b27cfce1c19ebbcd4 -06023cac3eff8c27b6be5ae413877d152b6142338a97f7092eacc4172ef19909c699477530bece5257366dfd299ea7f5a9d211363f7be9d16aff6e0c10c7f9e1 -ade9cb07b3638d7f387f9a1cded51dcebb57df2b6ba479c9e17c04ef9dbe7cd4507e65dbcf5a7dc672a66c71e5c1c314cf0ac847afe88b72b8cfed70efcb92b7 -c9374f30f239ac7374fafaa377b649fb60fc8c975b3df9317972ab89390e7dfd427ee8efdde73b771c4707e1ff006bee54e91fa622af88feac62f6c67f84e5b4e53387b5bb4456447f5436e3c352eb294977c7afbffd2387f7512b269fc3bbac69bd7bc75187f998fe3f57fe5bce88c4adb00000000049454e44ae426082} -}{\nonshppict{\pict\picscalex133\picscaley133\piccropl0\piccropr0\piccropt0\piccropb0\picw7594\pich8467\picwgoal4305\pichgoal4800\wmetafile8\bliptag276711534{\*\blipuid 107e486ef6f08100a0e5ef6a660d26cd} -0100090000034ab60000000021b6000000000400000003010800050000000b0200000000050000000c0241012001030000001e00040000000701040004000000 -0701040021b60000410b2000cc0040011f010000000040011f0100000000280000001f0100004001000001000800000000000000000000000000000000000000 -00000000000000000000ffffff00f5f5f500a6a6a600585858009b9b9b00e0e0e000eeeeee00dddddd00d5d5d500ababab007a7a7a00727272008f8f8f008484 -8400c4c4c400cbcbcb0052525200141414000a0a0a00222222002a2a2a00181818004e4e4e00676767006e6e6e00b6b6b6003939390033333300bbbbbb004646 -46009090900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 -00000000000001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010107020101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010107121801010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010d090101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010d00040201010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101070015010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010800000a01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010115000801010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101041d090012060101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010119001d0101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010109 -00050700140101010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101011a001801010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010102010e0010090017010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201010102161c0101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010104 -00010a000b0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010102011c1201010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101011b1b011800000a0101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102020d001001 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010600 -0b011500000901010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101011814100003010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010b0e1d000608000004010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101021d0901010b000a150c01 -02010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010708010114191f12 -010300001a0102010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010116160601011418191c010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101020101010101010101010c1c0101160514190118000008010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010e000b01011f150a0010 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010114040109120a000f -0115001c010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101011b16010109000d13180101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010109070101010606010101010101010a130a010d170300010212000b01010101010101010101010101010101010101070101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020101190302000a0101151b1c13 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010103001a010109000501010101010101191301011e0c181b01 -1d00001d0101010101010101010101010101010102010f0003010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010e00070b1101010e1b0e000f01020101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101090015010101141401020101010201011c1901011505001f010b00120601010101010101010101010101010101010107001501010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010606010101011710011b110808160f00 -18010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010118000501020c00080a0f0f0101010108130f011a1505000707 -13001b01010101010101010101010101010101010101011800090101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101011c0401010101010101081611011b0e1b1301010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010106001501011d000a1000001e01010101111b010111150404010b130018010101010101010101010101010101010101010103000f010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101041202010101010101011f16010d150e -001d0102010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010201010b000a0101161101150014060e0f010713050107001915030b00 -00000301010101010101010101010101010101010101010200170201010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010a001d01010101010101060a0106121a1217010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101181801141201011f00070d00001d030008091100080103000d000317000000100101010101010101010101010101010101010101010b00010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101160c01010101010101010d08011b0e -110001021d0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010105000d06000d0201160e02120019070003061300170101181b0c000a1e00 -001e0101010101010101010101010101010101010101010102161a01010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010f16070101020101020f000d010d1c1a001d021d010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0102141401180007010e16011f0013061115011e13131d01011b1d14001a0000000c01010101010101010101010101010101010101010101011c160102010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010a1e02021603010101010102060019011d13 -01171701010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010201010e000a0100180108001a0114001806001d0e1b0b16010204140600170b0013 -001f0101020101020101010101010101010101010101010101050009010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010108131b0103150101010101181b1c110101110a03000801010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101020101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0112120103000901041c0119001401111b02000a17171a01161903000c1900000010010101010101010101010101010101010101010101010101160e01010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010109130a011e0a010d1d010800111f09010b -1c07000c010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101090301010101010101010101 -010101010101010101010101010101010101010101010101010102010101010103000b011c110110000f1d00000308001d0b1b071417150a00091900051c0000 -14010101010101010101010101010101010101010101010101010c00020101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010201010316011a15011d0c020103181a1101030002171301010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010115000e0101010101010101010101010101010101010101010101010101010101010101010101010101010101 -17000606000a011c1c011c001502181b0800030e1e150e191c0800121a0000131e01010102010704080101010101010101010101010102011d01091707010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201151901040802010101020d0f150801 -15031d000f0101020101010101010101010101010101010101010101010101010101010101010101010101010101010102010105001e070f0102010101010101 -010101010101010101010101010101010101010101010102010e1001010101080011010e1601100007030000190213090500071b001409000a0a001104000000 -0401010101011e0401010101010101010101010101020101110c0103110101010101010201010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101011d1310010901010101051503170d01031701121b010101010101010101010101010102010101010201010101010101 -0101010101010101010101010101010102010612120918000b01010101010101010101010101010101010101010101010101010101010101080018010101010d -001d01000d011c1e011c0012061818011c1f0f00001d1f13010b0003140000000c010101011b1b01010101010101010101010101010101011d140110001a0101 -01010101020101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010601011817011f0a011d0a011614031101 -0612060d0006010101010101010101010101010101171b02010101010101010101010101010101010101010101010101010105000a19001b1a010101011c1f01 -01010101010101010101010101010101010101010101010107120d010101011e13010e00020300100a00000c0118080a13011700160216180117130300000000 -1a0101011e1702020f01010101010101010101010101010101181901110e01020101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010201010101010101160a0107131a010c0101100d1400051110011e1106001f0101010101010101010101010101011100190101010101010101 -01010101010101010101010101010101151d1e1e08120a010101010107000b01010101010101010101010101010101010101010201030d01070d060101020700 -0c011404011b1b011b130009091e01150b0800000c09001a011e1b1800000013060101171e01100d0a010101010101010101010101010101011a120201060101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010f0f01011f11010c0c0101180000170e04 -010a1301180010010101010101010101010101010101161401010101010101010101010101010101010101010101011d00010c1a1f1c01010101010101151f01 -01010101010101010101010101010101010101010118001d0f00080201011f001d090009090009051300180114041000100b000008041601011217120000131c -0101040401010c1d0106010101010101010101010101010101011704011b03010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010601010101010201010101011b05011c0901090013160f170207001a0800180101010101010101010101010101011f000a01010101010101 -0101010101010101010101010101011e17011e0a1b0b011d01010101011d0901010101010101010101010101010101010101010201090a0a1a14010801011c00 -0f1114011e1701141c12061000020b1702120004080003010513190019120017010b0b0101111f01051501010101010101010101010101010101100008051701 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010d11011a00060101041f0101010101011a15010a0b01011513140b11 -0b0117180117120d060101010101010101010101010108000e01010101010101010101010101010101010101010109000a05171013021a000901010101010101 -01010101010101010101010101010101010101011d00040104040115091000161000030119010a00141401191e0213080e00001a0e0007011e1917000c13000a -0a0b01010e00100106131a010102010101010101010101101001011c1901130f0101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010111000d01151001010a0a010101010101011c05010e0d011f130b160d17010f15010616001701010101010101010101010101011e0006020101010101 -01010101010101010101010101010500081e08041401041701010101010101010101010101010101010101010101010101020101181b1213130f0f13011f0004 -031501070302171700110600030304011500150613180106130e00171700000b0b01010d1f0a1701011b1901010e0e0101010101010101051e01011016010c17 -01010201010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010106131c01010101010101010101010102011a1b01011f0a0812090c0d -110701151f011f000003010101010101010101010101011f00100101010101010101010101010101010101010101181e090c0100040116100101010101010101 -01010101010101010101010101010101010101010c1d121213010d1c011c001f110d010b09020e17000519140714081a00000a0a0006010e1e0c001f1c000019 -01010c0401011706010e0009011d04010101010101010102161d0101040401120301010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010a131001010101010101010101010101011105011a00091e0a101c1c04011f1501061600130901010101010101010102010106000d010101010101 -010101010101090101020101010114030a1a0600041717010101010101010101010101010101010101010101010101010101010f080f151f130118181000000b -16021a160a1c0400000f151d051401040012071c1c01011b1d1e12091300180101051e01010110170307001a0101070201010101010102010b1e010109000805 -14010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010201010101010101010101010101010101010101011115010101010101010101010101010101110606000c1e170213 -171c07021305010d12120c010101010101010101010101011114010101010101081e06010102130501010101010800080d101a0000001f010101010101010101 -01010101010101010101010101010101010101151a1d181d1e01120b110000140b010c16000000001e03000814030115000c1d001d011d040900171f00000301 -1f150202020110000f011c0c0101190b010101010101010106001a0101171e01150d010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101071c0a01091308010101010101010101010c1d01011f16010e00000006041e1b0e020e1e0102070500060101010101010101010101010f001d0201010102 -06000c010101181501010201011014010d010c0000130f010101010101010101010101010101010101010101010101010101011a011803181b10000e15190000 -170a1111000000000c1c0e0b12010d1312021111010104050400191e0000061a1401080101011e000f010a1206010a150101020101010101010b1b0101091201 -03130701010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101011b120101180401010f0e01011d0f01010a110101010c04011700000c1d -161f160707130a0101011519010101010101010101010101011b170101010101011b1c010101091402010101010316010b010d00001201010101010201010101 -010101010101010101010101010101010101111f01181200171a001c150c000012161f0c00001414191610160c0114140e02161d0105130916000a16001e0913 -10010d0e010b17100c0107001f0101151a020101010101010110130901011c18011c110101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101011c180106121a0101140d010a16070101170b010101171b0816001402151e1319010c1701010209131001010101010101010101010103160101010101 -010d14010101010f01080e01010905010b0106151e130f0101010d080101010101010101010101010101010101010101011d001d011100001f1f000005110000 -1311021b0000181500111a001d030014011f1601011e1805001b0a00001e161a0101061e181b01010c1001171b01010b040101010101010101010e0e0101081b -0609001a010201010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101020101010101010108151001171b01010a120101160401010f13090101010c0618000010 -0c0019140107161d0101011804010101010101060201010101011b1d0101010101061410010101010108000f0d0b0301090108140a000a0101021c1c01010101 -01010101010101010101010101010101010b16010f000a0e101900120f160013000f08141e140a0000091700061e0004011403010816081b000a0e0000001f01 -01010118000701011f04011a000601061507010101010101010101110901010c1e01121e01010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101011f170109160801011717011d130901010411010101010118001611080005110c010c1101010108131d010101010104170101010101031607010101 -0101110b0101010101011b1a0900140705010f140a14020101010f120601010101010101010101010101010101070f0101000b01120c01010716001e06001e18 -00090c0b040c03001c03001e0a00001d1a1601010c0c0800000a11000019010102010f00160201010914060116180101190b0101010102010406010c11010109 -00090900050101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101060b0601021f070101010101010102011c09011f1b0101061203010416020108121d010806011e180900 -06111e1d1602011b10010101041c0101010101081f010101010102120b01010101011d150101010201010c0b0104140604010f160d1e01010101011a06010101 -01010101010101010101010702111b010500020e000701010a0000050f000d0b1306161b130911000d1e001917001b011618010614080400000a150017010101 -0107150a0e0e020101190a010e1201010f1501010102010111040106150901011717011900090101010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102011f001101070019010101 -010110090101030b01011c1101010a130801121f01010d15011a1501191b01120506001d1c0a010a1c01010108000a0101010102100401010201010a12060101 -0101011e030101010101191e010d001a19010f001e1e0101010101010d060101010d1c01010101010201010a1b131f011e0402000c010105170013010412070c -0f1000001602141c0f00000d16000a090009010a180713121c0f16001a010101071e030107170701010208010800050101110f010101010108120a0103040101 -09001d01131b01010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101011b120101050009010101010d130801011108010813030101111c010a120701010405011e190313011f -040115181f1c010111190101010d150101010101011e0401010101011e1701010101010a1701010102010411010e12110d010813000b0101010101011e1f0201 -010f1901020101010101010105130207001a0b1601010f031800180200170b0b010e00000e0800030e00001300120118180101150d0e1617181f001606010106 -150d0102010b05010101110301111c01010a0e0101010101010c1b01011c1d01010c1c010a001d01010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020106130b01011b1b0101 -010101171701011a0b01011a130801081205010c17010108130f01141512020216061f00081c0f0101141d0101011c04010101010201151d0101010108160f01 -010202011c0801010101171901181917020108000018010201010201031b010101010e0b01010101010101011818010b0006160d01061c0814000809001e1b10 -010400000f0b00021b000000001908130f011d000d1c0317051c001e010106161f01010202070b07010105190110000a01061e010101010101011c10011f1b01 -0107121a011117010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101051307010f000301010101011b0e0101171d0101190401011a160201120401010b12020913000501 -1b1e07000a1f1801010f1b02010110120902010101011d1407010101011f150101010101110d020101011e0b01111d1106010900001e01010101010101151a01 -0101030015010101010101021b03011c0c061407010e040500160105000d1407010c001501140e03000018190002191c010111031a1609031a00150601061219 -01010101141a1f0b010101170701171c0101190a0101010101010c0c0101140a01010c1401071203010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010517080101011b0b01010d1607 -01010101081609010904010101040301010c1b011d000e010118040110131c010a0007191e07120f010118040101011916010101010101111701020101011718 -010101011d1101010101141f011106111d011000001101010101010101181801020102140d010102010101011c0707130719110102150a171c000111181a1501 -011c00190f000f1c1512061b1802161d010f1501190c031c05000c01081217010101011100030214060101110c011d0010010a1101010101010101151d010e14 -0101061205010a130801010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101020101010101011500000f01011a140101011b040101090c010e0401010c0c01010f1b0201011e1801051309010104180813131d -0116050712060b1c010101111a0101011b0401010101010115030101010106120a010101011b1a0101021409011c071c0e011000001b01010101010101010201 -0101010314010101010101081501051102131d0105040e0b0d0017001a180e010800000f0e1b0f17031e1013021f1b0101041801000b1100120003011e1b0201 -010111110105010e19010119160101111b01011b1d0102010101020b1c01011b0c01010d12070119150101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010201011d191e140701011e0a02010716 -1d01011c1901111d0102040801010b1b010106121a0119140101011c13130018010b12011e1101160a0101071706010107161d01020101011d1609010101010a -12060101011f1b01010a1401081b070018010712001b010201010101010101010101010114050101010101100401151f1f120201180e1b061708170010141001 -0f001507140f1e0f1111111501160d010616080d001b1313001202191c010101010c1c01010f0f01141d01011b0501050009011005010101010101011503010f -14020102120501021604010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010d130f1a1801010101010101151801010a160101010e120501021c1d011902011011010101150e01011f1401011c1c0101071c00171e -070713101d0006031e010101090301010105160101010101010b1501010101010b0401010101060101191b010f1b020018010716001601010209060101020102 -01010201061d0101010101030c0212021c1701101b19170106010d1b1d1b01010e00050a12081e0f161f00051f1605081f040114131f1313000b0f1602010101 -10121001010f17010b1901011d130901160c01011b090101010201011d1701011b19010103160801100f01010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010218001519001e02010101010110001a01011e05020201 -09010101081c061f1701011f0c010110131a01010411010116180101021700000e011e1e011b19011e1a010201180a0102011e190101010101010b0b01010102 -011c0c0101010101011b0e01050b01000c01011400000901010b1b01010101070101010102031b010101010b04100c02131a010e0e04050101011a1c1b1f0101 -1513010e18050b0a160c00061e05050614091a00121b1c19000a170a01010107150a0101010f000f0115080101160c01051402010c0d010101010101011e1f01 -10131001011117020101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010106010101010101020f000b1f000e0101020101011f1607011a160101010f020102010a0e011b030101110301010a120801011c0c010900190101081300 -1e011d13010f00070a1c010101010602010107121f0101020101011b1a0101010110131a01010101061609011b101000170101040000050101011e0f0101010f -1d0101010101141f010101030b0b0b0f1201011e1d0401010201010e00080110001305161917061700000b1a11100b0315011b001200191c001b1b0101010612 -0c0101090b1b0317011f1801010c1b0101140d01071e020101010101011a1b01010418010109000e010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101081e19010101010101010110000f03000901010602010118180101180e0101 -11001a0101010101071e0701071c1001011816010107160b011a00040101121e150301150b011b18011a020101011d180102010f1610010101010108140f0101 -01010f160f0101011f1e010814011f11110101050000180102011d1e01010101010101010101051c0102010f1514030d1e011d1e050e01010201011d0401010e -00171c00170406131300091e0c1f051c1b0a00000c13141300000301010104120601011b000f010c0f0117080106160f010d140101190a010101010102011b0d -0109130a010119130701010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01080d06010101010f0f0101010a1c011b1501011b0f01020216030107161a01020512090102011d01050c0101101c0901010418010110000301071611011415 -1715011d1e010f000601010101010110090101021f140102010101011a1209010101011f00090102180d011f1b011105170101061b131206010101150f010101 -030502010101010801010110000005170c011f100b050101010f01070401010b000b1212131a0a00001e0d000811030000000013150013000016070101051208 -01011012121d010818011f1b0101191e0106130e010f1e0101010101010b101609011f1401010116170101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010104001d0101011815021211011f140101010a1401010e1602 -01010a140101010602010e0901011a1c0801011e1901010a001a01010c160000041508011c1d011e1c060101010101011b1a01010104110101010101010b1e01 -02010101191401011802011e1f011110040102071c00000c0101010d1f01010109150201010101010101011000001c121d011c021f06010101041d0105020115 -1c1a001411011b00000d1b0d1019180000001304140000141b0c010109001a01010612031d1c0101181f01140301091609010c120201110e010e1a010118190e -1101011c0b01011a000a010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010201010101021e14010101011c0d10000d011e1f0101011c0c0101041f0101020d1e010101010101181001011a1c020108120d01010a000c0101051300 -17160b011f12011d001c0701010101011d1c0701010115110101010101011b0401010101020d02030b010915011d11090401061a040013001c0b021004010101 -01040b01020101010101011000001515010f1c0101010101011a050105050f00050e151e041d0000000014010c110c0c1300001b0013001f051d010616180101 -011b0c01011e19010f15010f160701190e0101150d010911010e150101071110161d011d12060101111401010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010205000a010106161f0101010812090500060614080101081c0901021e -08010101170e01010101011a150601011f17010110120b010109121b07021300160414010114050117001c0101010101010b0401020107120c01010101010612 -0d0101010101010101021f0b011903100401010a0513001313000b0108010101011d1501010102010101010f00001115011a180201010101010819010f111800 -0f1c1b141a19170012130c011c000e1100130000000000040e010117160101010c190201011d000f01170501110301081409010e120101110301110a01010d05 -0d1601010c0401010800190101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101011012130801011012100201010315011916010d110101010d1b01011f1b01010106161d0101010101031c0101011c17010101040e0101081513051513 -1c0912030119130109121b040101010101011d090101011d000f0101010101091203010102010102010111090119011a0c01011a1b12001e0900170105090101 -01010c1d010101010102010f00000a0f010b0301030109040101110601111e1e0215191b10110d000014010300150500161113000000130003010c000801010b -120801010101191e010f1c0101110f01110b01011e0501091b0109160601061603160e0107121a01011900100101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010117150101010d15010101011819011c0b010c1f0101011b050101 -181901010103130801010101011911010102170b01010106010101010b00131b101818150108000a01040e040e01020101010814100101011f16070101010101 -10161a0201010101011f16010f180105170101011b00001307170001050c010201010f11010101010101011000130f06010308080301091b01010e0f01111a01 -011812191f00130000030111000e0d00041b0000001304140f01000b01011a1308010101010102141d01181a011511011d1206010f160601110b01191b01010d -110a1401010b1c0101011c1c01010102010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101020111110101011104010202071b0606161d01110f01011d001d01010c1c080101111c0101010101011e170101010e18070101061c0e01010e0013 -0d1404130901181b010914061e0f01010101010b1501010101111501010201020110000b01010101011c0c01040d010e190101010d0013000d0c001006040101 -010101150501010101010110001b1f1f01100201010102110101100d01050a010f180e0e1400001313020800000814160300000000000b00101c18010109121d -010101010101011f14011d1c0103150101040c0101040301011e1d011b1a01011c05151501011c0e010110000c01010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010102010101010102120b010102121f010101031c010e160106141d0101031e01 -01010c00100101150e010101010101170e0102010e130d0101101c1c0502101e00000e1c040109130901080108160f01010102011819010101021c1c01010101 -01010a130f01020110001a011706010e0e01010106141b121807001f011d0701010101051401010101010110001b1f03010f0201010101170901091b01030e01 -1f1f011f001300130c010c000c08000d0d00000000131600131c010107160e0101010101010101011405011e0c010e190109160901101102011a140109110101 -1d1c1d001a010f0010010105001d0101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010108070101011d000a01011a00100101011e1901160c011d150601011815010201031609010900030201010101021e0c0101010f13180101011f131b020d -000c180b1307010404010101011f040101010101011c1f01010102150c010101010101060701021b1b1601031701010909010102011b101f1b011f0b010e0e01 -01010101151d010101010110001b0f1d01010101010101190a01010c010a0e01110601040000000009070013100400081600000000001300131001011b1e0101 -01010102010519011f16011d120702130501110d0101191801011e0c01040b0101110e041306010b1b0101011c13090101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010c150201010103130901010c140101010814060f140601031501010115 -190201010a1c0801050010010101010101040801010106141509010101170014000d190c151f0108160801010101180c0102010101071c1d010102011c1c0101 -0201010101010e1b0e0b0216030101010101010101030b011608061e01091701010101010517010101010110001c010101010101011f09031101010c0f020e1d -1c01061300130015010b150f10000b1f00000000001400000b01010d12010201010101010a13121f01110e011e0d011f12010914090107141001091c01021c06 -01091608151f0102160301010f001502010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101011b1b -010101071414020102011916010101110c0101011a1701041101010c0c010108160d0101011a1c0801051310020101010201020e090101010e13040101011f00 -00140c140d110101041801020101011b1b1e020101021d1b010101010712170101010201010a131014081a14010101010101010101021b01040d011410011706 -01010101011c19010101010f001b0101011f0101010c0c011506010d1d010e0c0b01190013160003011405011900091b000000001312001e010110120f010101 -010101011b0b01110109160709170101150b01171701010b1401011d1d010e1c01011118100010010f120601011f000501010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010f001101010108141b010201011711010101130b020101171d011c0a01011b1f0101 -10121a0102011f1206010b00090101010101010e121a010101021c160f01011d00000d140a150d010616100101010109000b0101010101040401020101081212 -02010101011303051b0115030201010101010201010118030a17011705010e1f010101010103130201010110001b061b091e0202010d19011503010a0e011e15 -0601121c12001e011a16070700111f00160000130000120801061305020101010102010a1306010e19011718011919011a140201140f0101171701010c0a0116 -050106160f191306011811010101170009010101010101010101010101010101010101010100010101010101010101010101010101010101010101060d08011a -001d010101021411010101011504010209130d01011d1c010f1c0701021b1a01011d001f0101010b1406010c0010010101010101061919060101010d00040701 -17000c0c0c03001d01110401010101010a1e0101010101011e0d01010101080c070101020d1501140f091501010101010101010101011018011407031f011d1b -010101010101171a01020110001e011d020b0201011d0d011919011d04011c15011d13071e001f010d0a011f000314141213000000001d010104150101010101 -0101011b1b0101021c0906140702120f01171e010304010101190f01061b01031401010d1502130b0106161a0101080409010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010108001310010d140601050201161901010109001f010110001a01010c0c010b14010107151a -01011d13180101010e1410010400100101010101010103150f010101080416171400140f121d161f01101c01010101010118170101010101011e1d0101010101 -0101010114030f1501040b020101010101010101010101110f17030814010613100101010101011010010118001e011d01010f0601010101051501011a011c18 -01040d1a0014011d120201141b0c130300000000000401011a131d0101010101010105001b0101021a1101041e010b170110130f0115030101011f07010c1f01 -180501011c0d0a130801051601010201010101010101010101010101010101010101010101000101010101010101010101010101010101010101010103001608 -010c1b010415010f000b0104071d131001010a1310010111100115110101061c0a010106140b0101011d160e0118001a010102010101010f0d181a0101010618 -001e040f0b0b1f1e01071408010101010102171f010101020109141d010101010101010313010e1f01130f0101010101010201010b03010f0f0c04071308011f -0d0101020101011d12060700161e1d1e01010c03010703010f1502011d021b0f010401041319011111010d000916110b0000000012060107130b010101010101 -010812001901010101151a01160f01160d010d17010a12061d011f1901061506081608011d0009041e01011e0401010101010101010101010101010101010101 -01010101010001010101010106010101010101010101010101010101100000160a010418011511010a00101717011a000d01011a140f01101b0202150e01010f -000f0101010b15100101081515010e001f020101010101010105130c0101010116160d0c101418000f011f14090101010101071c0801010101020a1310010201 -0102011704071501051701010101010101010101110e01010e0f0301160d010418010101010101010b1d011603040b0501011d0f0202100101040f010d070b01 -101108131306071210011c0505001d16000000000d01011e1e01010101010101011700000f010101010514010e1c010f1407011e05011c110d1d011808010e17 -01181b01011b1807001f010f130f01010101010101010101010101010101010101010101010001010101010300170601010101010101010101010101010c0000 -000e01140b01130c01171510000d010c040102010e1401010b040107120e01010f16060101011f120b01010118131112001a0101010201020101090b1e0c0601 -0c000f1b011e1e161c01011f130901020101010e15010101010101031209010101010114090511011e0a01010101010101010102040d01010f0d010115110104 -0b01090101010101010101120c1c0003010103020101070201180a0110020402190e19001b010c04011f1c01141903000000131701010e120201010101010101 -0a0000170105020101011e0e01151f011b11010a130702120f1f061a180101161a02130a01090010030008010e14100101020101010101010101010101010101 -0101010101000101010101040000050101010101010101010101010101010f1c12131b09150d0f13030b000c0c001d010101020102141901011b0d01071b0d01 -011d1c0f0101020c121209010c00171200001a010101010102010101060c14050c00091b1a101c0e000501010b14010102010101171b01010201010103001a01 -01010f14011b05011c070102011a1c010101010104030101011e10011814011f180903010101010101010200181100100103130102011002011817010a090d09 -170e00111a01130f01150e10160204000000000901100003010201010102010f1600000f0f00090101010f16011a1c0106130a0111040719171911011705011d -1e01041b0101041b011b1c0101141401010101010101010101010101010101010101010101000101020101060404020101010101010101010101010101010101 -02010e1b0d130a1900141e0019161206020402031608121f01011b04020216140a010f161d010f1701111b01011e0c010c13001f01010101051c100101010604 -0000050e17011b15001c010101171e0101010101011c0c01010102010103001a01010c170614010d04010101010d1c010101010104030102010e19010b13011d -0401030801010102010109001a0300010114140101010d0f010a15011f1f0a0a0b191801011f1c010e0007040d07001313001701010c0d010101010101010115 -161416010e160101010101190d011c04010c16010115131e17031103091601011e0e011b0c01071305011b0d011d000a01010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010202011a000018151b14001b15001813150714040800030300090106131b020218 -00161a0b0019060b141d1c1b0702140c010a00001f07010101031c1f0101010118001b0816011a121500030101011e1e010101010106120a0101010101010a00 -1a01170d1d1901191d01020101190b0101010101140f010101011c070300080917010d11010101011a161c00061a1b0110140d0101010e1d01101c01051f0a0e -16121001011b1d01000e05140118001c15001a0109090101010101010101051e05140301141b0101010b1f08130606161d011e0c010f0f140301101e01040c01 -091606091309010d130110130f010310010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01011d1e120f06161b1a160000001500040f000d041307050801070f00151a01180016000000131100140300140207161f01091e001317050601011111010101 -10000008170d011500150801010101140401020101010312060101010101011d001a15030505010e0101010101180d0101010102130f0102010205110a000509 -1401020b010201010a00001303111501110301010102030f01071c010f1a0a0b00110101051b010c000f13030200131b0015010613030101010101010119000f -0e140103000c020201091c011811010b1b0109001a010103150101110c021609010b17010c1e01011518010b1606010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010108171f010b1c0d0400001315000d0f0015000c010101160f0e000003 -0f001f050018140013001415001c0707000e010104001c1300120d0105120a011a0000190514010d001c010101020110000301010101010c1601010101010101 -0f131307181a091901010102011b0e0101010107121001010102010416000e07140101020601010103001c1b141314081707010101011903010117021a170c0d -00100101121a07000b1f00010a00131b000c01040c0101010101010105000410001d01141c01010c02010d1802121d011403010e13070101150b010914061a0c -0107000a01130d010f13080116140101020101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101081418010e12060e00130000001717000000050101161b021c00000f0400090e1c0a0000000000150012070f000d01010b0b021d1912120710111e -1b001f121a000a0116001d0101010101031b0101010101011b1901010101010101050002050719190101010101160301010101061b0101010101010916000b06 -18010101110701010a000d0f001714120401020101011903010118071d130e171e01010d1c0104141d000d0118001300001f1a12020101010102011000160715 -18010a1308010d16010201160f0e16011d160601181c010108161d01051e011e0f010b160103130601041b011d001f0101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101011b12060707011e0000000000171700001606011d000b071512 -0e01140008190c1f00001400140d1315020f001618071f0e0701020b1610010e0000091b181e180103000d0101010102010e1e010101010102120d0101020102 -01061b071a0209100101010102141d01010201081e0101010101010101170a09140101010d0a010815000d0900090a000a01010102010310010118071a131213 -0f0106120a0200030d00060104000000001a1505010101010101011c00080e15010900180108001f0201011f1502160d011b1801071902010111150101041a0f -1602011c0b01110e01071405010c000a010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010102010117120801011d00160e170000141c13001b010118001a01160b0109131309111a07160000000e011e120210000016080f160b02011a131b05 -0e00091a160d1601011500100101010101011104010101010206161a01010101010f1e071c02090101010101081406010101011a140101010d0d0b1001170a10 -1b0101010f18010b0000090900060613100101010101180a01011e101a00000c01010e1c0117131a001f0101041300001e0e1b010101010102011f000308001a -011c14020118110101010101140c1f160109001a01010101111f1605010812080c1901081c020712100105000f01170003010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010117000a01010b1601011d1c00151100001818041a13081a -16050119001e061b1010130000001f011512060d000000100c0014161a060c1618000c01000317030103001801060201010101110b010101010109000d010101 -01051e0717011f0101010201020402010101010a170101011f1515030117101004010101080d01171616010900010f001d01020101011c1a01011e0605001c07 -0102161a080003191401010104001300151410010102010101181b1a011418010d1209010f13061917010101050006040e010b16011d0c010a171a1206010b1b -02161a010b17010c1701011716010715001001010102010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010104001001010c130802010515000000001216081714011f000f011b001b0b16060f0000130003011e000512000000040c0018041b0806 -13001601191705170101161610131101010101011e0501010101011d130d0101011d1e061801020101010101100b01020101010d11010101091414030117011f -0b081a01070607160c16011f000205000401010101011b0a010111010b000e01011f1b011c1b02140f010516161c0400000b0101010101010d0011010d160110 -001f0101150410001a010101020411100010010f11001c0101141a1e040106130d031301011c0301141f0106131f010f001c0701010101010101010101010101 -020f150a0100010101010101010101010101010101010101010201010101010101010101010101010101010d000901011c150101010118000000000017011619 -011b12071012001b1f12060f001918000e011c000013110000050c16061d130a1f00000f0f0003161001090d01091411010101010f120f010101010108140b02 -011d190811010101010101011f1701010102010c0401010108110c0a0117010b1a0c05010d1e08000400060b001a18001401050f01011e0901021e020b000f01 -011b0910000319190103121800051b001e0901010201010a00140414161a01121507010c15011e0c010101110f01001a1114010215001819060b150900180203 -00061e19010a1401101607011a1606010e000401190f010101010101010101010a00000d01000101010101010101010101010101010101010101100701010101 -0101010101010101010102010c120801011c1e010101011a0d1c0000001f080005060015010a00001b1b16010a000b0e000509000000030b00001d0c00060617 -0c00160b011b0c041c0101010601011619010101010513060101010102021417011d19021e01010101010101170b010201010117040101020111041a1004010c -08191a0119190a000c160c0b1e1e1403140f1f050101170701011b0218001a011d0401161509140709001b1c170800000901010101010300040f000019010400 -0f0110000309000f0102010e14010e1506161001180000140106000f03001701171702121f011e0e010404010119150101171c01170a01010102010101011d18 -13001208010001010101010101010101010101010101010101021b0018010101010101010101010101020101010b140701021b1901010101010104181c001d1d -000f090017020c00001200150703000810000c17000013060400001a04160107160005160605000a0003010105050109161001010201181e010101010101011b -1b0d15020401010101010101161001010101011818010201011f1b0d0108010b010e0701040c1b001a1913081d0014011002031d0101170701081b011f1b0501 -19090f0c08070a011e001b00080d0016060101020103001b0118001b011f001f0101171e01141e01010101011b1801151a06021a0012001101010d17011e1201 -18000d031302071b09010a0601011518010608010101010101010102010a00000000180101000109191a01020101010101010101010101010101061b000e0201 -01010101010101010101010101010412070106131701010201010201011813011a000a1700150a1c13001c1513090a000f0f000000151216061100161a141e01 -07120b110402000a18000601061b06011a1b01010101011e11010102020101011d0e1501070101010101011d1c010101010901190e010101010e111901010604 -010b0201181300130b04001d0200180f060101070201170701081b011f1f1d0819020f010608010b14120015011e121b0101010103001e011f00150608001307 -0103000903000f01010808010912080809010714001410111d01011c1116000e1c121206170c01051b01061c0801091406010101010101081a0a1d0101150013 -001409010100011a00000b01010101010101010101010101010101021b000301010101010101010101010101010101181b010110130c01010102010102010511 -011a0c07150018021c12001313131d11000f101600161f1c16081c0014051319010c121d12071c1809000c01010c1f01010d0d01010101011e11010101010101 -01031401010101010102011918010101101c01190d0102010111191801010a0c0111020704000013051c120901130a0202010f0401021102011d11011a1a0a0b -06181101110d10140614000c09140505010101030012061d00000f011800110107121108001e01010812120801090701010117001209010f1e01010e00131700 -001a1217181309011e1f011c1e0101070101010102011d00000011011800000014070202010001010c000014090101010101010101010101010101010215000f -010101010101010101010101010102011e141d0102151e0101010101010101040b0101010f1102010e001313131313100c000b0d00001e0104150c0000141800 -1d03001f110d03000704000d01070c0101011b0c010201010612170101010201010314010101010101010116030102011d1b011b0501010101170d0401010707 -011d0608180000131712120801130f010101080501071102010a0c010e0b120b06001f1f1609130f0f0000091f190c090101061600111d00130f011a00000f01 -18161e16130901011c1c1e1b01010102010d0017070101011804011d001901181607160000001e010f16071a0006010101010101010111131500090900000000 -081a001a0100010101031300160901010101010101010101010101020108121501010101010101010101010101010101021700060108121e0101010101010106 -140a0101011f0b01021e161b17120b010f140e0f0300001f0115000000000000121b0017091407151907121307010e0a010108120e0102010107151509010101 -01101c010101010101011a13020101011d17011b0301020101111f0401011a07021002071c1600171e17041f01131f0101011d0e0102110701040c010e1f0406 -0b000500031d0d03160004011e0a170601011b00001e12001701011d1a0d010f1c0616000d01010b000801151f0101010c00190201010101090d061500080104 -1810161a00171e17010b1a0106010101010201011a16140b000408161300001a080013070100010102011a00001e010101010101010101010101010101010900 -1901020102010101010101010101010101011717010109121701010201090c01030008010102181d0107111c1c09010109150d01010c190c0306131300000013 -130000001d14050f00020b000b01060301020108130d010101020118000b0101010915010101010101011917010803011a18011c0a0101010109091801070b1a -0d0102011c00131017170d111005080101010f1e0108150101110a011f050518000e1900160b0213000003021b0d1701061c1300130000001d010e1517100114 -0b0b001407011a000a01010b000e0315000c01020201010708021c00050109000f191102000501160d0101010101010101011011131f10161b011e0000000c01 -11000b01010001010101010914000c010101010101010101010101010101010e000f01021b1d010101010101010101010101011c0b010109001c070101021909 -011815010101081406010605051901050709000d0108110a15101013191c00180a14140c0c05140114170202110301020101010110131701010102011a000b01 -010105010101010101011403010d1b011a0b011c0a010101010d0b1c010a0b0a0a0101021e19140614140f141b0102010101061e01101401011c10010c050500 -1409140014010b001814011f110c0c01170013000013001a0109120013071a000a12000501091317010101011b00000019010101010210191917001f01010e16 -070003030002010a130801010101011f1b16121f0106161606040013001b010d0017010101000101010101010916001101010101010101010101010101010101 -141b01011916010101060201010a0f01010201011604010107151e010101070401011504010101031e01010f1b1f0f10180109130d0106110e1501180e020018 -010d131d120f160d03000301091101010101010201011b1c06010101011d1d1d1e1b1c060101010101081408010b040101010111090101010a1c181c010c0503 -030101091e1a001019000a14170101010101061e01101401091501011f1d1d001a0d00001a0115000f06011b1d191019000000151d170301011c13111801161f -190013070600001001010f1b0000001f010101071d110d071c0003010107130d0a000204150101010e1206071f1716001b0501011d0017021100000012070a00 -1e0102010100010101010101010f13000301010101010101010101010101010107121f0107120e0101140501010a131d0101010102150d01010217000a01010c -180108131a0101010e0b01011f130c0e0e0b01101403010f11190c01110a040008011415000e1f160214000f010d040101020101010201190005010101011a00 -1e15000f0101010101031b01010e040107020117080101011f0b081e01040f051d01041e150500031a160a000b0101010101061e01101e010a1b010105081a00 -041c0005010b1200130f021e0614140012001b07010101011a00050c1d0e130d00000d011800000d0604130013001901071d021f0b1c041c000a010101041307 -16190113140101010613131c1c111a08010101110005011800000000050b000401010101010001010710010101011d0000030101010101010101010101010101 -011f0006011f1302010e170101010a001a0101020102140e0101011f001b0101171a010a1207020101041f01010c040f100e030108161a010e1b1e1707191b00 -1f011900000e011b1a1a001701011b1d010102010101010109160b0101091510010104040102010101040d01010e1c011f19011e1a01010111080a0b0104070f -0601041e150d150e0f000a000b01020101010118011a1b01030c0101040a041313000c01051c180000070a180f0000000017020101010106161e091e01030b00 -0018010a0016121300121716050909191b04020111131b000b0101020712030d000603001401010212170a06010101010105131310011b001c13001f0b001b01 -02010101010001011a0003010201010e00000f010101010101010101010101010101191801011e0c0102121f010101031402010101010614030101011a140201 -061706010e1502010107120501010c0d0104000a010f120f01181617110714001c010500131206051601170011011a14011019010c0801020109040101151801 -0101071d0307060f0104070101031c011a19011e0a0101071c010b1f0817010c0f010d121b1f1b0b01001d1e040a0a01010d0517010e1b010b0b010104031500 -001e010f151d160004011800140000141f02020e0701011113010b0e010111001e010b001c1c130a0a18180d0b1519030601010411011004010101011e150212 -0e0115001101011d12070101060e010617001707061b13111400050e001408010101010101000101011c16010101010119001610010201010101010101010101 -010102160f011d1407010d12010101010e00090102010107160e010201181d01010d110101151e0101010a16080101191a09131309011a000a010b171404021c -001d080000001801001f010e131d01040b01181a1b131a01010101020d1501020708010f000218040109010101091e011d0b0105060101020f011e1d05190118 -10010300161f0c1b011e1d01031816100116171c02080f011c0d01011908190015060104090b00001a011e0000001f01010117150201181306081c0101190000 -091f001201051c010f180d0e050701010106110c010f16080101010a000f1715010e00000501010b150102011a150e00141a010500000c11001217001e070101 -01010101010001010110130f01010101011e001b010101010101010101010101010101031b0101191101011c1d010101010b13090101010101110c0101091c07 -0101110d0106130d01020105150801011c1a06131208011f000b010b1e141b021c150e00130016011f1607010a1501061407071a020e001d01010102120a0101 -0d0d010e1c011c100a0a010101081e011d1801030f010101010115071105011910010900001c0a14010c04011c150b0101160e1902010101161d01011b080212 -1b010b0b0b15161610060515041a0101010b1607011f1308010e0a011a0000040900000b011b001c18050601010101011a130b0109000c01010107141a0e0003 -01120000090101150b0101011100120e0102050000041b0000000011060101020101010101000101010118120101010102021c00050101010101010101010101 -010102011b1801071603011a1602010101010d000501010201020b080101030e0101011c08011d130a0101011900100108160f08151502011a000a0111141518 -02160013121c121f01001101010404010318010101011d130f01011f14010101170901191a081501040a0101010119011005010b05010201011d150715080119 -0901071200001a14081d161a1c100f02011203050102010f12020102110801061f050c09141d1001100b031d01010101030a06011f000f011d1501061600120f -1500001e1e000501010101010101010e120a010d1e1a071c1a011b1b0a0017010e00001e0101090010021a1500190102011800120e14001300131a0101010101 -0101010101000101010102131e020101010106160011010101010101010101010101010109131d010d160101191801020106070300030101010201010101010e -1a0101090c01010a1309010101041310010f131d010e1507011a001801191c041e1200140e110e16010d000901061203011b05010101011f130601140501010f -15010115020511011c08010101010b020f04010b0b01010102190e1015010119100101110000091e0e011112110102100212030e010101060b0101011c060101 -0d0301020902011004161102010101111b01011a001a0101150d011c000011110000160000170d0a0102011007061b1b06010b1502090b17011e130515140208 -1200001d01010c000e1e0000130601101c13001f11000000041a19020101010101010101010001010101011a000b010101010103001306010101010101010101 -01010101010d1207011e1902021c09010110120a0000190101010101010101061e0601010c0010010513060101011b12070110000a011715010103001002001f -1d1600130f1503160f011b1606010a1307061c08020101010e0605140101010411010915010c05101c0102010101111d061e011d170101010115100b11010110 -0a100105001b010a0e0110131901071801160f01011d17010101010915020101190d010202070614000e010101021c001001010c1d01011915010c0013000000 -00141513141414171f0607191c120c01010c12100a00001917130512130901171c1e1e010108160000040211170719000013171c0000000501020a0101010101 -01010101010001010101010105000801010101010c000b010101010101010101010101010101111701091305011f150101011817030000110601010101010101 -0511010101140e0101191401010110001107010f1e07011c170101091902040018010415010e191917011a000c01011917010e1e010101010101120d01011c00 -05011f19011708100301010101020c0c011e0107150601010a1407121a0101090d03011d000b0b0f17010a001a01061101041d01010e16010101011f12101101 -100f0101021a1e000b020101010b150801010102010101180610001616001511000b1c00151d0801080105001b09010a121b080c00131717171a1b130f010e17 -031e0f010118000d07010900161600001b0d120015181d010102010101010101010101010100010101010101010d1409010101010111000301040d0101010101 -01010101010102160e010e00060115180101011c110e121c130a010101011a0601190e01010815080101171e01020119000d01011a131a01150b01010a1b0a13 -0004050005061403121d0104001f01011505011b0e010101010f1208010a15150901181f011e02010101010109011f04011e0601151d010118040a1502090101 -0f19010216181818121001131a01071101040a0101031601020101091f010306010606101f190004010102010f0b01010119090102010201010b000013130d15 -19171c00050101010108051a01061e130a010c00130a01061c00001d0105000d1d0c01010813190707071700000000040f160018010101010101010101010101 -010101010100010101010101010115160901010101011b130711001901010101010101010201010300080116040110000a010106140d121b1d15131a01010d0c -0108140601011d1b01011a00040101011b131e060103000501121801011f1300181f00000010180c1112010212130901091206101401010101111b0101111a15 -07011e1d09040201010101081c011a1501110a01110d010114050101070b010909090201161e0a181618011e1d01010401041901011d1601010101010101011e -081900001e0e00180801010418010101061f0108100101010c00001314090f080e17111c01010201010702020e001706010a15000a01010a131709011a001e1f -131f0101181301181616001313000e0f16130d01010101010101010101010101010101010100010101010101020102170019010102010813110214001d010101 -01010101010101011b1b01061605010a120601010812031b170104130902011d07010f170201010c171e001612040101081200150f010a000501121501011f00 -1607050000181a141f0004010a151502011f1b01061a010101160e010f17091c010114091d040101020101011c090912011819011a1c011a000601010614010c -050101011500091b001e011e1d010705010404010110160101030901010101190c0a141600001906010205001f011a120d0118181007100d1c1c1b1c09090706 -19191408010201010202021d0b0806170c0a000f0101090e0801011012120f000b01011d0003010108140000151d1f00000a0101010101010101010101010101 -01010101010001010101010101010101040011070102011d001f071212060101010101010101010102141b010a000f01041401020105141d121b01111305011d -1801011b1e010101111414001b121f01011d130014080109151a02161c07011a1314021c0000101e0a1e000c0101151801011c0b011e050108121001110a1004 -010104011a18010201010101111a011c081d1701011c050803010101070b01031f0105010b0009181e1606140f01010b010e1e01010f130601180a0101020107 -170916001710010109151c0d010714000f11001e1e141c00001e0902051a0f19020003010206010102020202010e001111001f010106160d1e06011e00091900 -0f010700170101021700130e081800161d01010101010101010101010101010101010101010001010101010101010101010a00130e0101010e00100612120801 -020101010101010102071218011f160601151e010101191b011e141a13170201190d010616030101081e0611161f161d01010a00001c0601011711021c000301 -03001e120000191e1e0a0000100109131f0106140f061c0203140e0000071a1801081e010a0c0101010101020c1a011103011e01011a040101081b10011a010e -0407001808000f0a00000e1410010111020a040101081606010e19010c0301010b0a1e0b0101011a001702010914141f14141a140000001c1f17090c1d08130f -1b0c010d151c0e051001010219041d1c0005010101150c1f1c0915001d09000b010119001001010c0000131e0000121d01010101010101010101010101010101 -010101010100010101010101010101010102061800151d010119120110131b07010101010101010101010900050118160802161e010101171c010e00001f0101 -010801011a000f0101051e0614141b160e0101050000140901071c1c0904001f011c000000000000000504001b01010a1206010f17010f1514121e031e011918 -010f1e0110100101010101010d0d010d0d01110801010101011700060219070011011c000f151f0600160e1b0f01011b1010110101011c09011f040117190101 -031e1403010611000d01011f00001e1315011a001416001111131c1d1d14060b1601011c00111d1b010103181d06150009010101171e011c1c12160907001b01 -0105000e01010f0000131912131f0702010101010101010101010101010101010101010101000101010101010101010101010101101c001108011b0006090014 -07010101010101020101010d001a0118120207140e010101111c011016160a0102010101010d1b01010104041a001212000b01010f1312120a01010400100216 -1216001f1b000014160c1d141b0401010d1701010e19011f00000b031e010b0e01051e0101010101010201010a1101111101181a0109100101141c02071e011b -1e011f0017111b01121e17160f0101171d010f0101011c1001051402021e10010114000409001608010f1c13191200050101191e011c12131f1d071d160f1700 -08021b0000041e0e1f1b0d0203001508010101181b010b00001308011e00080110001b0101101300001c01010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010205140005011217010a001e01010101010101020101011f0005011715020114170101010b000a01091b09 -0101100a0101191506010112050300040d120b01010b041d001901011f131f01040000181e0000150814081b0d1c05020117050101141a011500100e0e01180d -01050c010101010101010101071b071413010a0e010a1801021c0b070717010f1e0108130011000a1e150c00100101190f01010101011c10011a000701111e01 -0111110b100d091812120c051900050106121e021b12060202010f1b101f000802121c180000001b190801180018010101011b15011a130b001f010e00120601 -1415020107150013000b010101010201010101010101010101010101010101010101010101000101010101010101010101010101010101010514051d0017020b -001f010101010f0501010101010500050118131d011c170201011b0d01011f1607011014060101141b01011b13071816161b160401011819071215060110001e -01040000000000000b181b0f1b0f120f0108160f0110150a130001190e010e030106080101010101010310010616060f000d0f1901080302010c10091d120701 -1c05011e001b000c0d1c0300070101191d01010201011d010109000f010a000801031f1f0e01040011080300131a011d121e0204001c0202010c1e0104001d08 -121c0a0000130b01010915120f0101010915140202121f11110105001f04061b120901071b001400001001020101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010201010108011f001b061c001f01010201010101010101010a000a010b000d011c1b0101050c01010117 -1e01010d15010110000e010b0014020400140d0e161001171b021e000301071e1b1f000000160013130a130d0419191601010a1607010e00001901181a011805 -011d05010101010105001001011b19011b1b020f02010501021a020c01170e010c19011d0013001f021c09120f01011d1d010101010101010101141d01060003 -01060c1d1c010a07091b0015100103000b0f120b1c0a0117001c010c001a07131b011b0013000901050017020101010d000d01021c0d1814010a000510141713 -10010a14001b170018010101010101010101010101010101010101010101010101010101010001010101010101010101010101010101010101010101010a1313 -1200000a03110101010101010101010101010103000d0117001d011b09010102141f0101150d010110000a010c000402171b000e1a1001011b1b010e000e0201 -05000013001f1714190e0d000a0f011c180101190c0101190010011809011805010d0e0101010102140501010108131a07131d0101010c020104010c100f1c01 -0616090114001204010403170a010103050102080101101d0101141f0101111d0101111d1c1f071b00000c02010e000d0b000d1819011900001c1e001a011512 -071f001c13180104001f010101021c0003010115040e00061a0011061600130f010d00001c0500000f0101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010a0000131c00130004010101010102010201020101010f120f011f00181404010201 -091707010f1209010105000a01180004011e000018010101011e1b011016160801061700001e0e15011b0d121c01020216050101170a010a001001170801051d -010e0b01010101031401010101010c1401041c0101010c05011b010304011603011e0c010400121c010a0d0a0e0101031801061701191c0f010118180101070e -060119191a141b001e0f01071c001d1016081817010d000311001508011514011a001c0313171200100101010d131c080107151619001d011617010e13130f01 -050000000b1e000e010101010101010101010101010101010101010101010101010101010100010101010101010101010101010101010101010101010101011d -13131c0000000004010101010101010101090901010c001901061813000f0101011919010119160101010b000a010c0004110012001608010101181210010c13 -0e0101170000101e061f1b0f00110101101507010f14010d000202170801090f010a0d010102011e1f0101020101011b1f01121f01011a1b011b0f010e010c17 -010d12010a000014010814091a0101061f01011d0713030101010f140101010f1d011a170116180f01011a00130f01161f0512011f00120e001407011c14011d -001609170016000401011a16130a010109131919001d011b1b011d00000a011d0000001817001501020101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010201020e1300130000001901010101030101010f000b02010b00161c160a0a160702 -01010a03010114190101010c13090114000000000000141d02010118000a011a121b070a00001e0b1101120f0f000d01010b1e0101051414000701170f010519 -0101010101010614020102010101011a150111000701061b010b1a010e0906120808001a011400001a01140a010101010101010102161001010108000f01011d -18010617011f1801021900110110130c1f00060300191c000b07011714010616000918000506130a0516000c0101011a00190d001a0115001111001b09010814 -0000180a00001a010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0201100000000013000401010117140201010f1400190207051e1e1e011117010101081610010f130a010101040010061213131c12000000050101010d000d01 -011c001b0000000d140f121e0105130801011e1901010b00130801190f01100a0101010101011f1e0101010101010101141f0c001b0f011b10100f011d0d0115 -17011517010d00001b010c04010101010101020301170301020102121f0101010c02011e0f07130e1c001a0119000419000a0e000306130a01071c1407061616 -061a001d0119001b00170801010110130c18001d0214140511001407010614000017061400190101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010e000000000000110101010412100101010b00001e1001010b0c01141a -010101031209010e0004060101110007101c060e000000000018010201091311010b000c140000190b0b0d000601181e010106120d010900120601191f01020f -0101010102011b0e01010101010101011f1405000011010d0d0110010218010e00091d130c151312001001140801010101010513011819010101011e11010101 -180f011805011c001502011b00031c000a0e000306000e01021c1c0108121606081313111c001600180102010105151d19000a081614070103001001071c0000 -1e081c001e0201020101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010c0000130000001c08010118001f010101011d161e01010117010a1306010201041401011e00180101010414021f15091b0000000e151e0201010913 -1e05130105001e000e1c061c0e01011e0a01010f121007001c0106141f01020d0201010201071210010101010101010107160512000006081407040601190a08 -000b01121515040e000d010c0d010102021a0500010a1b0101010105160101011f1f010a180119001a1a150c1015150203001c10000b01061b1c010913160606 -120b060d000c06000a0101021b1c091b000803001702010d000301071c00001b0217001507010101010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010115160a05171300120f010103121e0f1a1f0f01010101011f1c011e -1b010101011604010812001d0101010d1e01041b1012040019021c000a0101091300000c10001700110b0e081c0701081c080101160c09001c010f000b010207 -01010101010a140101010101010101010115050c00000a011f060505010f17011512010c1e0204050e1e010314010101091b1a1201021c070101010215100101 -090c01071b06011e00120a100017010b00001c000b01081612021d00180109001801010d1b01191601011a161703121b0919001901021f000e01101500001502 -0c001806010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101011d0011010102091c0018060106140013001b020201060101041a02130c01010109160a0105001209010101040d01180c100016131c080c120b0101 -180000160e00120b131a0401051e01011f15010616061f000e011d001e01010e0d0101010118180101010101010101010119170500000e01100c060b01011706 -05001f0713061e1517000901161d0101101b08001a0211030201010115190101010a01011b10010c1b010d000b1015000013000501081214020a001f011d000e -01010b13011d00090219161f11000c0a16000d0102040005011a13000017020c0019010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010e001c09010101031313050611000013000a0101110a01011e07 -0a00030201010f000a011f00110101080d1c0a01120d1f120c141b0106161e01011b161400151508041919190117180101191e1a1b010b000a01091719010108 -0801010101150d010101010101010101010d140300131502011808180f011f1f01151c0117191000040018011f141d01101c01150e010517010101011f140101 -01191d01181a011a161e000b030000000000001a0912000e11001d0103120501011f001d02140015141105141b010a00160f01061b000f010c00000004011100 -19010102010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010201010d00131a010101081700001300000000000a010f1b07010b150117001d0101010c0010011b000b010207061b09021c080b1605131701020415 -1d01111600030c1908121f130801150d011a00001c010c000a010108010101010101010207120f010101010101010101010a140c0000000e011a0a031801061e -01030007061c0916041e001001141201061e010b1101081607011d010615020101191d010504011900121a0b00000013040b0015140000120005021b15100101 -0e001a011e00040a0a11000d0111000406010814130f011e000000050916000b0101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010a13000b060101010f000013001b080c1501010d1701011c -0b02151c010101011100090816000f010101081b08091c071b140f151e01011a130c061400190a1301170d171b0108131c1519001c010c001a010201100f0101 -010101011013080101010101010101010106151b161c141806010310151001181807001c0212180c13050415101d1308011c011d1c0101040d01171f01171d01 -011d1f010912161515121c00000000040f160000001c03090404001802010118000a01091312090a121e091d13000a01010d00150203000000000b0b00161a01 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101011d15001408010106130000001c010118040101140c0110000f070004010101011e00061f0015010102011a1b011a1506151e0615150601 -061e1517001c01160a111e02131f08130003010e150119000501010f1d0c0101010101011000100101010101010101010102151c1e0b161f0b01050d041e0102 -15070a00080d12101318061911011405011c0f07140601061f01091c011d1d010110190101141a011700000013000b1f13000000120a190016151f0101011b00 -1d01011e001e1e001c091e001907010111000d011900000000000000110201020101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010102010d00130c060d0013001305010106131a01081c0701 -0b1b011d000d010101061600101a0018010107060c19011f0d021615010c000d010103000000080b171700060a0000040c08010814010b000e010219071d0101 -0101010110130201010101010101010101011c1b130e000f0b0102150e140f010e1801181f01160f1100041d18011e0501040b01040f01020101010b1f010508 -0101110701191917120000001b1d18000000001c081a110a1a0801010f14131d01011f160801071213131e0901010a1212101015000000170e0c1f0606010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101061700131300000016020f06011a140701051f01011b1b010c00030101010f001c011013190101111a190e010d0c1d1606060a00 -15110d030000150b130a001b1e0013080101010114070a000b02010f01010101010101011013060101010101010101020101191913100901100c011911191101 -081c01080d010b180b0015031e011919010c1b010c0c0101010101071b01030b01011110011f160f041313001e1e13140000111100111f04160d011700140901 -010d00100102170013100101091c000d011900000016030101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010101010101011d150000000014010b180101041e0101191a -0108131f010400080102011a0011010f001b01080b01110a01111f010e001a0b1312001600000013000304001c090e0e0101020117101a001101010101010101 -0101010106131d01010101010101010101010e191502090207110f0c000f1b1001180e0104100213001c000e14010d1c010f15011d1b011c050201010b030117 -10010d0f011a17020a00160000000000001b0c131c001c04001115130d0101010f000d01101c0004080f101900161f0b16000000040201010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010102010400001300001f011c1d0101140501091508010a1308011514010101011d000d01090003010701071b09011e0502160004 -1b1d081113000005191316001e01010101010101041f10001e01010c060101010101010101141d0102010101010101010101090b15081e01060f0c1f17010e11 -010b130d040a011b000d120e1e090a1c010115100104081f000f0101070b01050b01050301061e0102001c051b00130013000018151c021e000017010102010f -000401011100110d1613001e0f1b00000000001f0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101081500131f1c0008031c01010815060119 -1501010c1202011515060e0d011013030105140701091d091b02071e0f0f000000120f010a00000a081300040e010101010101011f0c06000009010201010101 -01010101011b0d0101010101010101010101010b150118011d0801011e1d0118091b001c1c11011a000e1a18180507120601170e010e0b010b16010101070101 -02010f0b01011e07021e011d00000000000000000b011f171b13070101010f001e010108000012130000191f1e000000120d0801010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010614000901150007180c01010c170101151801011518010f00171a000f0103000d010414010105010f15060716081d16 -1e000019010d000c17001b010101010101011d011d190616160e01060701010101010101010c140101010101010101020101061115010e010d1f01010511010a -180a00130c111a011c001e12130501121d011f1501101801011c0c01010f0e010101080b01011e09011814000013130000001509011a121d161a0102011f0004 -02010111161a16130013171600141b1f070101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010101010101010101011d12130b0e001e02141d01011e1701 -06141a0108130e0115001e0b18020105001f01170c010107030b1e02081c080919060e001b031b13001c1d010201010101010406071102171b17011805010201 -01010201010a150101010101010101010101180015011901090e010101111001150e13000a0a18010d001219001e01181d011015010119070110150101021e06 -01010119010217030e00000000130000001609010213111b180101010300170201010e001f0c0013000301151401010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010201011e00151800030f150101060019010a1501011000050f13000e1a1e01020a00171700171d1d000b0b15010f1406 -021d021d13001900000f010101010101060605060104071e1813070b1f0101010101011e0f01150801010101010101010101091c1c101c0106170101010a1901 -0b041c0002021b06011612011900060a0b01011e0a011d1d01010e0e01011f1a010102170901170e0e00001300000013000901011713121c0101011f00170201 -011d000e17170900120111000a010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011013000818000e1c1701011d13 -1d01041e0101030010081400040e1702020a00000b11000d0613041f18010e16021019060104001300000b01010101011a03051a01181a181e131a1f0c010101 -010101180d01110b01010101010101010101010c1c101b010817010101071702020d0c000501180b010e170101141a061e01010c0b010608010102040201070d -010101171d011b0c04000016161717001f010f1613001506011a14130008010105000b0b14011e00121c00170101020101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101011a001b0816001f130b01010e1501011b1101010416010711000d0e11010105000a011e190910131f1f05011c -190108171d061300000b15150101010105091a18010e181f1c1b1908170101010201011f0401051401010101010101010101010c1c101c010817010201010c0a -011a12001e021f04010b120a011a1608161d011d1c0101010101010c0d020218060101181a011b15150000110206131e01091500001310011d00000013080105 -000e0d0006191c1b0000001d01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011f000d08001b0b12060101 -1e18010800040102111509010400051917070105131007131b010f001f0e1807141b070617151312001f0109020102010e1d091b011a1b0715181c081e010101 -010101061101011c1d010101010101010201010c17101501091b010101011d11010813001207021e071a160a010111051e0c01011c090101020101091c07010b -0b01010b0a011c041b131902010c131f0b0000131506011100171d000b010500051f150717130a12001c09010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010216001a0a001200110101071519011f000f010216000d01170019161609010c0009071c0c0105001f190b -08001506071212150c160c01010101010e0a011707071c01111f1b0517100101010201011908010e0b010201010101020801011115071401100c01010102010d -09011900000a011e0d010a1706010a0c0f1c0101040d01020b0101011c0e01101801010505011c13131101011f0016160000001210010d001d011c1b010b001f -0b13070d00101e001209010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101091213090a0013000d01 -0109000c010c1c07100c00000e010c120000000a011f1209011700181d120b180b1a00130516141308061512030101010e0301190501141d0319051f0e0b0101 -090901010e0e011015010202010101081309010c140118011f0b02010101011d1f011d000017010e191d0d00050108150f1c08010d1e011d001d01010a1c0101 -1e0601050b0600180919011d130000000000000c010d00030111150704001e1b1c011f13111100000a0101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101011d0013100d0013000301011a001d011113131c1500000d010d000505001f010c12070915000c090018 -1e1917151300080e1303011d14090101030b01101101110d01160f0b1e0402010109010108040101110e0101010101011e18010d150804010d03010101010102 -0e01011c0014010a11051d1e190101040d181f01081701021b040101011c1a010d0301090d061701061c0d0000131200000014070b000a01171e0715150c0015 -010d001513130d070101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010e0000100d000013 -1001010c001001111213130000000b0103140807160b010c13131e0e000b071b1c1500151b0004040014141a011a04010f04010217011817010c0b0b00150101 -0201010101171a0109150101010101010312011f140818010d03010109100101190b011f00001010001a071800010110040f1b01010c0901061e0801010e1c01 -0a1701080e080d021e0000001205040013130c1b0004011800041612051b14020d00130019010102010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010201040015010b0000150201011e1207011c0016170c12000b01031409011704011f00131818001806 -150017000017110013041d171715001c061801010510031601081d0800000f0102010101010a170101040b01010101011d13010514010e010c030102091e0201 -0312080115000509000d01021c0f01011c0f0408010e190101081e0601071c06011801060c1019010400111f0a15000000001300000d1400001b0f1f000c0104 -0000001f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010100010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011e001e1d1700 -001e0101011e15010d0011090c0c13001102101303010416080f13001c0f17131a1900000016031c00001e011a1c13120f1806010f1b07121d01020117161e01 -0102010101011e0f010f1502010101010812020f14010b01191d0101010d1f0201131d010d00040900161002161b01010d18040d01090d010101191901010e0b -010c0801190f18010a1c02051300000013000000000014110d0d1b0005011e0000000e0101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010106160000070400001901010111110211000d0d1919000016070f1319010c001d08150017011d -141a1f13000000120903001408091903100b1001011c061b0b0101010108001a0101010101011e1901011b04010101010812020a16091c011809010101080c01 -01041f010f00160b1e16190d00000501071c0304010101010102011b0901081b010b0301170a1801190000131513000000000000140a0101010b00050f160013 -130a0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010916001706 -1b00001801010115131d1900081015140012130f021519011a1619011400121001040c071e00000017100a00190219101f1f0d010118180a1c01020101011719 -010101010103001608011d130601010109170103161015011708010101010d10011f1101080000161e1e190f00171806011903040f0109180101020d1b010111 -1a100c011e0a19010b0000141313000000000011010101010d1c1c1b000013001001010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101011a0000141b0000000b0101080011010c001002141b011d151a011c15010215111d1b0d16 -0b01041b091a190000001313120f0509190a0401010f1407151001010101011109010101011a1704170101171701010109180103121d1b011708010101011f1f -01071b02020000000c03040112191005011d1e050e01011c1d0101011105011d11080c011b0d170117001300151405170000040101010814000000000000001f -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101030000 -130000130005010111000c0111000a0f131d01081c0501151701011f1b191910151e010513181900000000170f030310030f0e070101171a0418010101010119 -190101010101170517070106130a0101080401051c091e0111080101010106180101041a011213000d010801041502040201140304010110170101011d110101 -1b1f0b071b09170a1600000000170e1600161018011a001e09091c0000000d010102010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101011d130000000000131d010211001e0119001e11141a01071c11010c1b06011c000016 -1e161b060619000000151705010f0f0a02080e0301011a171a000901010101010201010101010b0e03040101051308011d04011d1b0919011106010201010104 -0f011f1a01150000050101011f000e0a0c0119131109010206050501010f0f0119040b0b11010c00000000000000040416070c001e000c011a15130000030101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102010f -150000000000000c01011900170109131b061b1f01071517010b1c0f1f15000000041b0003010d0000141f07010c060d0219190b010101141d18170101010101 -010101010201101701151001011f08011d04010f19020d01170601010101010a0d01091f010a19000e0102010418150f11010f150608010101021b0a01101501 -08121610010215000000000000001616060f00130019090400000015100101020101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010703160404000004010111001107101305011b1c02010e1e0210131f010400 -00000000001c1d0d1517001e010e060f011a0c18020108041b09130901010101010101060d010117091a1207060701011d1f01050e0619011b07010101010101 -1802010c07010e000e010102140a0d1c1c0b01151a0101010101091409011e0e011e000801090000130000000013140f03000000001b000000160b0101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0201010101010e00001506020e14000d10151a0118001a01031b09081311010e001b1400000000180d001300011a100f0f0211111f011f1600090b1e01020102 -060101010d02010a0e011e1b1b0501010303010b0e081101040101010e1001021909010c0c1d1d001d09090100170114001e010e0d010101011a01111e010917 -010a000a010b00001c1a15130000181413001300130000120e010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101020101010f1c001c06010913130207150c010d131d01091c0e06161102 -1d1b1e1b00000000130000001008030111020d1f11010118121e08160901010619010101100701021807021c000501011818010e0c1a170104010101101c0101 -1f05011a001a0618011810101616091f131e09060401010101170c01131a01181d07001f011119130c04000013000016160000000012191d0101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101071c00121d0109141b01011b1b010f001d0106170d0119170802151b031400001e101100030219010b0a02050d10010205000f0312010101 -0e0a010110050101050e01021c030101180a010e0c1d110618010101010c0d01090d0110000f05000f18060d1d1f15011b131801040f010201091309181b011a -0401120d0102011c0000000000001314130000131e070c0006010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010102010107190000100101111409010b121d09160d010111140702 -1611011f151b0f061c1b1e000e011710020e010d0f0101011f14001d1a0601010f040101020f0101011c1001030301010b06010e1f0a18060401010101091701 -010e0108001d1800001901041d01120f0f0000061004010101011f1c061e090117071e0c01011a00130013001c0c0c1300161f06010a00190101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010c00040201010e00030105001d01161b020715000a0111120e130011080117000e0b01040c010b091d0c0102011014171401010101 -01180701010e0101091c04010a0e01010406010e050c18100401010101010d1a010d100114101c00130a01110f010b15011b0003011110010101011503031701 -0d0d1f1e011d000000131908010e0000180701010813150601010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010c131300110101010a131901101b0b011c1310010400 -18011d1700000000141400100b060a0c010a18011c080101010409151701010101050f01010310011012130f060e01020407010e030318100401010101010a11 -011a1a011110120000080211010201150307001e011a0d010101010316071509021e0e12010111000e02011d1b001e09010101101c1306010102010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010a130012001b01010106131b0202141e020c000d01060b1b0f010f140000000000080d0a071101011c081a1f0101010e0e0614100101 -01030b0101031f01010d1e1e060a01021702010b03031807030101010101071407060b01040c151700070811010101061c071e000501100101010101151d1f0e -011e0b1c0f011e131f0416001b070101011d1700001a010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010615131b131209010108141402010c12011a130e01 -010e001c06010f1f1f0e1610090d011c080105110101030f0103190102010201010818010108050102021f031414020717010111050a1f080301020101010117 -1a0118010a13030c13011d1a010108010c190f001501010101010101031e0211010a1b1b0f011200000012111f1f17151600120d1d0101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101111c0917001a01010204160f010a151a01191b1a010804001e08010101171d010e010d190101151f01090c0107190102010101 -0101030101021f0207010106191201091101010b05050e0d0d010201010101050c01190910120f0515010a060101110d06120219000a01010101010102040218 -050114001f0216160e041e1315171717110302010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010111140203000c180c011012001f0715180109 -161318081d150013140e130f010d09010c01010f1408010a02021509010101010101030701010c07041a010d1d1e1a080401010508030e0a0f08160201010109 -0c01050901110f050401180201020913060e1102140401010f010101011f19091401190011111701010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101020119000c081c000015030704001f011b140801170000170f0d0000131311011d1101050b0101051f010101011b1d01010102 -01010b1001011909071702090c190506110101050903040c06081b01010101081b01031d0111031117011b010201010e17061208180008011001010101061507 -1e0f0f00000e01010101010101010102010102010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010111000b011100000004011d001c1e0000 -05011a14000012000000181502081c01071c0601010101011d01181d0101010101011a100101181a011d17020c00020f1c01011f0a100013060f050102010106 -15011d1f01040b16110f1901010d1b0116050b18090018010201180f01011b1f1a19010012010201010102010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101011f0011010b000000131f0313121600131b10011d1e00001403030405011810010e1101010101010d070e0d01011a0e -0101050d010105100101180b02000a101c010106011d0015011d0c01010101011c07100e01040b00040c0c010106001a1a041d16011e000801011f1701011d14 -0819011b000801010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010e0011011a131714150d1f1e1218 -120012191d0214001415111d1102180401010c090210141001011a1801010a0b01011a0c01010101010101141a11190216060201010f00140105180101010101 -1c1002130911110014140f010101051c011b1d0b0a090005010101151a01011e10191003000a0102010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010105001c0707171e001f0a0a1f1718040000001400001f1114071c0007180f01010201010c160107091801010102 -0101100b010103090201010917051e021609010702100016010b0d01010101011c100900100e0e000016020201010218030c171a17011c160701010a17010119 -19031808001901010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102011d00151015161e001d06031a -1116191e000000120901041f0300021a1b020110010101160d020111080101010101060b01010a090101100105121601041d01010110001c0118030101010101 -04090a1410050d1600170101010101021c1b1307110610001e0101011e10011d16101e09001c0101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101011100131316001604080d1e17150c0e19151702020e131c000d01171710160101010d1c01010201010101 -0101080b010101010101010101040010051f020b0109001501040a01010101011f08010c0b050e16161d0101010101010300131d1f04011b1c0101010b0d0101 -111017031e0002010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010108040903130000141400150a -031c1400160b070d13151500130f01010703031101010109120601020101020101010911010101010201030e01021317011c071b06060013010c1a0102010101 -1807010a0e180b000401010101010101011500171016010517010101091101010d110d0d1f000a01010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010107121209071e0000130000001e0f0d1c00001316000013131501010101010305010109011b0c060101010101 -0101021901010101010102180e011900020e030c0606001601041a01010101060c0101100d1e14000e01010101010101020d0014010403011609010101041001 -1d1c0f190a1308010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102010b120d01030000000016 -00121e10100c1b1c1b0000181c0101010101030e010104100a00160101010101010101040201010101010101140301000909190d05011213010a09010407011d -190101101917140b010101010101010101011400070a140117051d0e01031f0101151a1e03001001010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010d040f0f1400131215120013150b031a0b001e0e1801010101010a0c0101040f0213120801010101 -0101011b07010101010101010314011e18071b1d18011b00070d10010301010e0b0102010e0b181d010101010101010101010c00110716071a17021101061901 -01040505040005010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020201011015111f000000 -00141313171200000013170e0d01010201011a0401010101011e0301020101080801011902010101020101070e000303000a1b1d1b061800060d08011001070a -070101010c051d01060a01010201010101011d000007111a0118071109010c08021d0d0f0d160501020101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010a151300131c1200001515120a110011110e01010a0101071c07010909010e190301010109 -1a01011708010101010101190000141900110a061b100b00111c0f010f010e0d010101010c120501061410010801010102010115000a0a0d010d100303010503 -01011e0f1c000b010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101190000 -1b1f1f1712131212000011110a011a160201010403010201010a121402010102020101140301010101011a0000000000000003010c0f1a000a1c1a0101010e03 -02010101031e0b01010d040112180101010101180003061701020a0f0b01080c01011b1d1b001b01010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101040013001909010102081400190505010c1f0102010a1c0101010106141802020101 -0102010c0e01020101011b0000000013001e1e010c0e011b1d0c04010101030901010101090d0f0101011e08051206010201010e0014061707011c1d0b060104 -060104191b0000020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102010e00 -000000001b041818160019050b011801010101010e0101020107141a0702020119070106100101011001180000000000000e01011f0b01170d1a04011d010308 -010d0801060e010104060319010c18010101011d13140d191007000a0a1a010c0a010c1919001501010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101020118000000000005100318000017030b081701010101010101010101011e130f0a0801 -19090101010101011018091300000000001801011011010d0d061e0119070a06011c0801010b02011f1407170801150901010108161f0a1a1f0700040a0d010a -0b010a110e0015010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020101011800 -1100171700181d1800001e09110a0d01010b08010902071a01010c140a0f0d011803010101010101010d191613000416001401010211011d110117101a061a08 -010e02010108010101110d190a010b0b01010101120a021015011813190c01091901101515001601010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101141b1b0013051016000000000c010b0b0301010b0d01111d010f010104110a021101 -050b01010101010101061218180e010a14151d01010b07071402050c05060f0801050f06010101010107170e1707061e01010101161f0a07120f0a04161b0101 -180108150b0000090101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010910 -121500130b0708081e0019010c12010101061e020f06010101011f150b0104061d1101010101010101010902010101071106020101041f01141f011b19080b0f -010e19170101010101010d0a1414011f08010101161f1a09180b0f16161510011e0607150e00000f010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010f0f0c1f021b0014051500131f0119151c010f11010101010b0c0101011d020110001e010b03 -0817010101010101010103100101010716080101010a15011e1b0119001a0d1d01110f170b0101020303020a070e1a05050101011b0b091f0917060c0c001a01 -1e1001040a0016010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102190e0e000013 -1e1e021500131312130015010f0401020102011d0101011d09011a001d011d1c01171a0e020101010101191f100801011810010101011f0801160c0c00031a1d -01040f1d1e011001101e070c011d191d180101011b04011a010b060b05001f01191d011611000301010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010103160c041e01010a001b1a1616180014011d170101070f01010101010e11011f160101011c -1017101e1801010101011905180e0101010101010101010201050000000d031f010c1a011b010409011b03131801030f1e0101011e1101010101010b1d001801 -0d1a01120e160f010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020101010f1c1606091e04 -01021c000c01021f140000021d1101010711010201010103140109030102010b000003011c1d1f0101011f1a19040106060101010101010101011c000017030d -01030a0104090f0501031a130401170d1e060101040401011a11080c111313010d0d011205000501010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101100b00001c1f00001f010b13001a0101010d00001d080c0101010b110a01020106150601010101010a -000003010c050c0501011d100e1c010b0e0101010101010101010d150d131517011d03010d1f0219010d0c131a010e00160701011b0c01011c000d0a00161701 -0d05011c05000b010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010604000000130b18000e01 -0e131b04010101020115000d011901010201191901010101191d010101010101151206011a191d14010103090813010a0401020101010101010109121d121213 -1f0108011a0d0111010e1b1101010500120701011b04010119000c0f001411101f0a08151d000b02010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010714001200000b011e0301050003120a0101091606190019011b100101010f03010410010a0d010102010101 -010519011a19010e0101170608130e0614010207010101010101011500000013001711010804010c1a1c0005010614161e1c0d06140005010800121a0013121d -1f1a05141d000e010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011d141304171c1e1d120d010e12 -10010d121001010416160018011b0b020101010101170c01101606010101010102021e02070c010b100711010309111d1b0f0f0e010101010101011e1c1b0000 -051700070204010c1713001b0413001214161814000014010712120f13001e060a0f181e1d160801010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101011d1313101e1501011e040103171001010c16010101071600000a010514010101010b01031b01011e0e0101010101 -0101181d011f01050f1a050118011d1e17180104010101010102010d0d0b00000c10000a0117010b1a1d1c00171f0d031e11191600131609041f161012000a01 -0d1d1c04010102010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201081313080b000f010d0401030c0101 -010f141b0e100c0105000005010714080101011108011707011f000f0101010102010e0f010b0105191710031801011b1c1b01051f0201010101010d0c0c1519 -000819030117011f04060e00001c0b1712130e0c000d14070b071e1a13130b05191d001f01010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010111001d06000301061b02031801010101180a0b00151c1b021c000e0101170e0101021a0501191f0101181608010101 -0101041f010401051318011e1001010a1c1b0c01150601010101010d041e0b01141113190111010e160d0e091f140000130f0b1e1b1a001019101b0a1313111b -191d0005010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011f00110111040101111b021807010306 -0c15011500000b1b0c03001402010a1c0102020204011d190101010c0b0101090801050f0105010b1901050b01010101190013020b0b010a0601010b18170501 -040f0f15020409121f03040b04140014000d02011b051b0c0b0f1b0500000c1e1417000501010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010713130705120701101c010a10010c0b0a0008090000001216170600000301011509021102150f011c1d0101061e01011a -18010f06010b08151a011e0901010101021c00040104010d17010104180206011f0c0113100d081316191e1e04001101081e1209130305151a09171a00001a16 -1316001d010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010d131f01130c0101110d0f0c0108170816 -03011a1600001300000919001e02010c0d020e0d0d19011811020101181d01091101181d010b05140117180101010101010912001a0a1101110a011918010201 -1d16081318041b131f0101011a1b0701011a0013130303001a09151d00121012131d1a0101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101011513011d130101011c07191a0101010d0d010d1a1f00131515001f1000001d01091c0102110d1e0109130502011a1b0102 -180119070f051b04010e09010101010101010900001a1605061601191e010201080017130000131b0f01010b001a0107010413091b050814011d190d0c0b0712 -000f0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010f000301190c01021d1501070201011f171f -060c061b000b180300190f00131a01011903011a0d191d0117000c01011b0a01181e1701181813100101010101010101010101101e0012001a1105061d010101 -0112121200131316001f1800161b1b161b00121a13101f1c080d18191b1b011c000f010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101011e00020118100101041c010101010117000f01010a000004041b0015101e170a01011d1901011a0d11011d161508011a0d01 -0c00100613001101010101010101010101010101010b130012191b010106020101121f1900160c001300000b0810160016081a12130217151f151915001e0111 -12020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101020101100004010818010101111101011008011a1b01 -010f1404001c0b1413130e11040a0101011b1001171a170f011e000501100d010a00010e001310010101010101010101010101010101070c001c1c0b010f0b01 -011b0000130013130000001e17000000131119131c0118181d1800151c0e01160014060101020101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101011d00030109030101011e0801070f01010e1a0101150a18001808030b13191e040d0101010c1e18001a0d1e01060a0b01081701 -0a00081b0004010101010101010101010101010101020101011100001d011c1f011b000f081f130000001312001c1814131413001e01071f1412160000171d00 -00130601010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101180006010a1f01010a0401071802010f190101 -170a10141e1e06010b000b1b0b04010201091313110802000c010103010915010d000111000c010101010101010101010101010101010102010215000010101c -010910010101080413001209050701070b0000001101011f13131319001c0d000016010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010106001c01010d1f01010d10010b0e01010c1001031101180f0c1c16090400191c1104010a0b0108060102011f0009010802061e01 -1700020e000d0102010101010101010101010101010101010101091213150910040101010801010118000010010101010a13001707011d0c1314121813131c1c -00131a01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101011d000b01010e0e01010b02011808011004010111 -0f031c05150a1601180019001305010115100101010101010601011d07060c0100110212000e0101020101010101010101010101010101010101011013001310 -111c0101111f01010115000f010201011c001e070e0217151c0014191311190000000e0101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010f001f01011e0007090401091801010e0e010702010f1010060b1e0119001813000a01010b180101010101090a010101010d0b0f -000c0300000a01010201010101010101010101010101010101010101100d04001f17170201110a01011719010101010f0015071d180f121213000d01191a010e -0b1c0006010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010103001d01010305010517010e0e01010405011f15 -0601010106161a0104000d0000090101011b0f010101010804010201011e0b0c000b16000010010101010101010101010101010101010101010101010101011f -0016001c10091406011c1801010210151505011706051c001b1902011e0701190217000101020101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101021f001a0a010101010e0e01170301020e0702010d1b010f061d17010103130e001c010101010b1801011f0601110b010101141513 -150813001c010101010101010101010101010101010101010101010101020101091e0000001514140d131d01010214030605150a010b0e1b090101100d010f11 -0117001d010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010a00141e010201010b1a011e0901070f0b1a0102 -131a0f1701010101011400000401010201061408010b0e011d000501101215000b02130011010101010101010101010101010101010101010101010101010101 -01010f19050b1400000e0b1a01051f0102140d010300000e0101010a0901111a0116000501010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101011a12150402010201181d0118070110001f010101131e011e1a0102010f151100050810010101170c01071e06011c15011a141500 -0b0112130b01010101010101010101010101010101010101010101010101010101010101010201080b000015010101011b180102041612090101030d01031e01 -1013000b010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010a00001801010101041d01180101031b01010106 -0500061d1801010105151700090c000601011d1d01010e0d010e00081d1e1312180d1b1a0a010101010101010101010101010101010101010101010101010101 -010101010101010108130000190f1b13131001031b17170101061508011e10010415151c01010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101070b121801010101180f011f01011b1a0106151401041f011e0601010d041b000300131701021807010110110105001d01110008 -14000e01010101010101010101010101010101010101010101010101010101010101010101020201060004120000130000160f0505170901020108010b0c0102 -14101800080101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201111901010201180f010801081e01011f0407 -010f030118030101151e1313021419171a0200030101101e011d1c0d0111110118000f0101010101010101010101010101010101010101010101010101010101 -01010201010101010d000316000000001403161402070101010101081408011f1701180005010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101111901020101110f060501030b011d1c020101010b101c11011d00000000100c1f1d1710001901011018010e170d011c1401 -0d00020101010101010101010101010101010101010101010101010101010101010101080d04151e1300000000000000000b0f14030101010101010b0b01011e -1d011e000b0101020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101041101010101110f060501171a01041b011a -0e061411000401010712151d090c19011405161601011019011e130f1f1c151f0a13020101010101010101010101010101010101010101010101010101010101 -01010d00000016160c0b0311000e1a141f0e1403000d01020101061b07011918010714000c010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010416010101011105010601170601151d01131e071b05001801060500030102131e011104040006011a1f08181b1e000f1916 -17120201010101010101010101010101010101010101010101010101010101010106001300000000141c1c000d01010102011115041501010101061d011f0006 -01190000110101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010102010a0000001d010102110e01080104020118010f13 -07061509001b1a000017010105001b0a1911181801010a1a1f181c000b010a000016010101010101010101010101010101010101010101010101010101010101 -01100000141a1d0a06031c0d01010201010101111214020101010201090018010a00160014010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010a001812050101011f0b0101021e020218011d1401101e0112161413000a01170014071b1214050501010702050500000a011000 -0011020101010101010101010101010101010101010101010101010101010101010f001c010101010101010101010101010101090015010101020101191b0101 -14191000160102010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101161e1c0b0101010f1e0101010d010618011d18 -011f0c01130b0e0314011a130314021000000f061b02010706170015000809000003010101010101010101010101010101010101010101010101010101010102 -0108000d01020101010101010101010101010108001e010101010101010101050006190014020101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010111131b170101011f00090101010106110102010104101013000e0e1e01110c01190801140016010c0c01050e140d0100100900 -190101020101010101010101010101010101010101010101010101010101010101010d190101010102010101010101010101010f000501010101010101010900 -19011600150101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010a00141208010104001f010101010814070101 -191c01040000030b0302160605120801141e1b0b091601181d140701160910140101010101010101010101010101010101010101010101010101010101010101 -01020d0005010101010101010101010101010111000801010101010101010911010300001e010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101071200000d01010f001b0101010201160f01010f0a020011050f0c07051e011112061e0003061702161d1a1d0010010c10011a -01010101010101010101010101010101010101010101010101010101010101010101021c001d0102010101010101010101010114120101010101010101010101 -090013001c0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101030000150101011e00100101010119030102 -01011a00030b0c14010e1e01011b0f040003081b011b09010e000601010102010101010101010101010101010101010101010101010101010101010101010101 -010101021c14010101010101010101010101011304010201010101010101010109171a1213010201010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101011c15000d01010500160901020102010101010118000a1c181501030501010b031d000d0912021c070a0013060101010101 -020101010101010101010101010101010101010101010101010101010101010101010101010801010101010101010101010106000c0101010101010101010101 -01010a00170101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010d0015000401081c111901010101010101 -01080000091c041402010101010e0d10000e1a000f12101c00150101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101020101010101010101010101011d000a01010101010101010101010102030018010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010102140000000a010c080101010101010101010d000401160c1402010101010e0310000e10130f12131e0504010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010a00060101010101010101010101 -010110001e0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010e000400140103190101010101010106 -18001309011b1b1401010101010e100900090d1c0115001a01010201010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010201010d1401010101010101010101010101011f0017010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010106120a0e00040911010101010201011700001e0101110d0e02010101011e060a00091b0d1d001e010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011f14010101010101010101010101 -01011f001d0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201010b1e01170005110a01010101011700 -000d0101021e030101010201101601191208001a1414070101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101020104140101010101010101010101010201170006010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010b12000901111b0c17010101010a13170006010101180c01020101010c140117171a001a001f01010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011b11010101010101010102010101 -01080016020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201010d0000110101050b17010101011e0e17 -1e0101020119170101010101110e01160d0e150f000f010201010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101121f020101010101010101010101010f001101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101011500001d01020b1c0501010f130a000a010101010e1c0109040101150907121d121a1f130101010101010101010101 -01010101010101010101010101010101010101010101010101010101060601010101010101010101010101010101010101081310010101010101010106100101 -011f001a010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010500001e01010b1f1f020115180513 -01010101020f15010a12010a14010f1b0711011c18010101010101010101010101010101010101010101010101010101010101010101010106020107131b0101 -01010101010101010101010101010101011f0001010101020801010f131401020111000201010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101171c150d010d0301010e00101404010101010101151a1b0b011e04010c1b01010f001a0101010101010101010101 -0101010101010101010101010101010101010101010c141c13151b1b1316080101010101010101010101010101010101011913020101010201020c00040b1b16 -07141e01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010103120f15010f1f0101161e02000a -0101010201010818000707120f011b0c01051c160101010101010101010101010101010101010101010101010101010101010101071200000000000000001e01 -01010101010101010101010101010101011e150101010101011500090900140a01161e0101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101011c0f110d0105010a00060a1401010101010101011504010d1b01011b071a000005010101010101010101010101 -010101010101010101010101010101010102010b1200131c11111500130003010101010101010101010101010101010110000a011a0e18020215000314150201 -0c001e01020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010f000d1f15010302161401041801 -020101010102181601011b0a01030002190016070201010101010101010101010101010101010101010101010101010101011d1211030601010101070a170015 -0601010101020101010101010101010912131a150016170201011d1b130d09121518001001010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010d001b091101031a001f01140a01020201010714001901051b01010e150215000401010101010101010101010101 -01010101010101010101010101010101010101010101010203051d0101011d180701010101010101010101010101010c00181d0e09010101010101011d13001b -09140004010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010216000801010512000709130701 -140301070400000f010706010106060b00001a01010101010101010101010101010101010101010101010101010101010102010101060c160000001b1f010201 -0101010101080b08010101010102010c0003010101011a1e120000131d1d12141400000011010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010201011e130d01010e0017011a1b011a13020115000016010101010101010b0000000601010101010101010101010101 -0101010101010101010101010101010101010101010b00111d1700000011010101020101010912001f0101010101011c0007010101030000160d0e1313070216 -17140515001d01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011b131701011a000f011f050117 -11010b00171719010101010101011b00001e010101010101010101010101010101010101020101010101020101010101010101011d031d11150b0c1116001f01 -01010102010214140801010101011f001308010108001c1b000b1002121b011e1214080c00190201010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101011e1917080803000701041d01120e011e001f180301010101010109161600190101010101010101010101010101 -0101011d16131c1c041f0201081415040801011f00130e1c00000019081c171017170101071613080101010101011e0012000e010a0015181300000b1c001414 -10140e10001401010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010b19140515131b01011e100900 -070600001512060102010102010e0014001f010101010101010101010101010101010716161c151515141319011b000013170a161e010a000000000012000000 -000b010a1200110101010101010800001e0a130c18151000000013001300001901171d1a00001a01010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101060f14181703041419010106010500010300131415010101010101011b000000090102010101010101010101010101 -01010106020101010101081a0101101100000000001e19000000000000000013050104161e00000801010102010c000f1e0c011e00151a130000001300131417 -14001a1400000b010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010516000000001304141303010101011515 -010500141917010101030b01011b0013040101010101010101010101010101010101010101031614110d0b12180201011a000000000000000000130000130000 -1c00041a1c000a01010101010214000a011e0c01081e13151313000013041d1d12150c1616001901010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101011f00160c0a080f0c000000030201011a0018010c00141d1a010107000b010312000009010201010101010101010101010101 -010101080101061d1f0c1712001b07010109181600000000000000000000000c10070b001b1d0101010101011000001608011b120f0102011a1a1f041e04111b -1313131f13001b020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101021800001d011a1b1c171314000c010101180004 -011c1616020101010c0016121300001b0d0a01010101010101010101010101010102011616040701010101081a1c000e02010101090e171b17041f05051d1d16 -1c19000b010101010101010108001b0b150f0118000d0101010101011d040407071e1f0513001201010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010e000000060c001b1e00001300040102011800040115040b01010101140013131519001906020101010101010101010101010101 -01010106051b1d010201010b151600000018031a010107010101010a1f0a1f16000000161002040201010101011100010f001f01091b190a0d041b14141b120d -0101071312161b010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101020111001b1a0d001d0e00131316000c010201180018 -011504030601010713001513041d001a191a0e01010101010101010101010101010101021119060101010101090a0a0a191b1414170000161e17131c04131300 -1b1b1c0d060f0f0101010101010e001c080114160901190000000019010101051c0610111d121e01010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010111001f01121701150019010111130b010117000b0111040f0601010813001413050112170e111401010101010101010101010101 -010101011e00161f010102010102010201021b151e1216151400001b04150b0701010a0e1b0014010101010101091300131f021800110c00140e1d0101010a1e -0301031c0b1613060101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101011c00001b001d08000c191e0d1100000901050e17 -010411010101010913141c1b1d011b00030c170101010101010101010101010101010102010f0000140901010101010201010d0c0d0e05080101020f0f0f0707 -0c0e0f0d141503020101010101021900000012170b1700000018020201010d0b0e1c031918160016190101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010112000000000f0d00191e1918140d191d01010104011f140101010108141f170b00181c0003031601010101010101010101010101 -0101010101010b0000040201010101010201031c1b151b1c1717181714001300130b1d0d000c01020101010101011a0000191800000d181e0017141709010d0b -0e1500160711000000160a0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010f00000d1d00100c001e011d0306010a0501010201 -011012060101010612090d1d111c121c1102130f010101010101010101010101010101010101010918120017111111100101010201090202020f0d1e1c1b1300 -000000001c080101010101010101010c0013190a1812131e000c1a1112110c0506010b1f1a1c0013130014020101020101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010b001e1c18000b0f00170c0000001b050e010101010101170c01020101151d110e0316121f1c021104010101010101010101010101 -010101010101010101011d171e170410011b0d060f00151e171f0f0f1f191c00001408060201010101010101010101010f1b00121b0c13000000120411131613 -0000131f110005151300000601010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010106000b1b1515001d17000019131718001605010207 -0101071d010101010e05051101181115000c0f160101010101010101010101010101010101010101010201191503051511130013151300000000000000001316 -1b1001010101010101010101010101010101070e1600130000131e1115000c071f0508011b1207121115130f0101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101011500151010001b0413120c001c16041b1c010711020202010101010106110f1b01010f140000051e050102010101010101010101 -0101010101010101010101081e00000000131319010f0b0e0d11151313130d010101010101010101010101010101010101010101020f1d1d1f040401010f0f01 -01010101171e0b000c03000101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010102141e010101110014041b0000000015131304061c -070617010101010101190b0b0b0112001f0c001415010101010101010101010101010101010101010101010101011d05050f0101010101010101010606010201 -01010101010101010101010101010101010101010101010101010101010101010101010111001e0f1518001f1d01010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010f1617010114170101010c000013130b1200001308011e10010101010107170a0a01040d11011c00000a01020101010101010101 -01010101010101010101010101010101020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101011c000f021600000000160d01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010f111d0101160301010e160000130b10041d1f17 -0c011d03011902010101061c09010e0f11011d001609010101010101010101010101010101010101010101010102010201010101010101020101010201010101 -010101010101010101010101010101010101010101010101010201010101010101010101051e1c151c001e1e0000001801010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010108010101170c010a0000001b05011002021f0b1e0e010c081b0e010906011117011b0f0e0201150a0101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010b00000e0e13091c000013090101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101011a000407010812080b00001e010e001c0f1b1313 -131e0c0a110814090f1101111e0d1201070201190c170d0101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010102010118000d17000e160c0a0000001b100101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010102010b00000f01010a1c09160017010019010300111e05010c1c141f091c0710121210191c01070102031b1417010102010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0102011d000013110916141c02130013001f01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101011a120d010101010c11071f111a00030100040107 -0f06010c0013191c001c130907131f1c12021e0e1b00120701010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101181504130910001314011b0000001b0101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101021d0205000401020101031c0812030114161d0b16121a0d041304070117110f001602120f190c1c0e0101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101150d01011a1405121306181c0400001d010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011100080f1c080101010101060108010f13 -020216001702011d121a0b141607050015011c1003040e1201010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010200180101181a0f160a0118110600001f01010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101050003020101030b01101c06010102010f1c0102000e0102010400170b100110111014010405071c10130c01010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010813131b04161a0a1b011f00140e00000b010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101080e010102010d1201010f060a0101010112 -0f01181d01071700130f010f19140a0116090511010c1a0500070101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010109181b00081f1710110b140c00151e000c02010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010102010102010108000401011c1c010101010e1a010201011b000a0f0f011c000019011b0f0915011a1c01150c010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101061e150f01010a011b001d0f12021d000a010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101071908010905010105090101010101 -01020101080a0101010119000e1d00041e0f011c090113090a001001010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101080313150a020102010516011c1f0119000301010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010102010a0c000e010101010101010101010101010114090d110101011c111818011e001218010e180118190900150601010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0813001313001f01010804040116080115000f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011e140011010101010110030101010d1e -051d000b010c18020202010b0507140012100917000b08161600000c010201010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010400161315031e0e1b0000040f00040b00190101020101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010d001001010104130e0201010a0c1713151101010f0419100311111600001318160000001f05040f12001f010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102010c -0000041203011a13121619000e040000140801010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011e1407011f131d010101090808 -0806010c1f0102010f050e171400000a1f000015110f0b13131a0f00001b1f020101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010102011b00000013001e011f00161401111701151306010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010809181f131d010518131200001510011c09010101060f010d000501151b1002010214001108021600140012 -07010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101190013 -0013181a18041619170016121e011616020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010600151f17190000190102 -090a07010b19010101070e1b0000151c170b1d010103000009011e0000111a1d0201020101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010201010103000013000019010101140a01091d140c08000016130301010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101050019081c001b1e010101011a080718010101020101100f0f0108171d01010300000304000e14000d01 -08160e01010101010101020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011013131200 -00001a011a1c1b0b1d0801151d1f0000150011010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201011f13061a1319151002 -010e00000c030301011a1d01010101010101010101011100001808011e131407010b001d01061b18010101010101010101020101010101010101010101010101 -01010101010101010101010201010101010101010101010101011a1300150f1100150401141a01010c151700191600161f001601010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010201010500001c140801080315000c010101010102010101010102010101010712000e0303161a160a -01010018011b121009150e0101010101010101010101010101010101010101010101010101010101010101010101010101010201010101010105001e0c000f0b -0008191e150109171f01021400041611100016010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101011f00001c0a01 -0101010d001b17091a100c080101010101010101010101150000161600070d040101151c1c140601180000190101180010010101010101010101010101010101 -0101020101080101010101010201010101010101011d1c0b1700000b010d15011c0d010d1409140011011a001b1e120719000401010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010600040b04080102010d15011b0d1300001d010101010101010101011000160f01100302011b -09091211000f01011e0000000e01050017010101011a0a01010101010102071a0f0101010f1208010101010d12070101010e0e02010f160000120b151409130f -0a0006010c051600171804130000141d160003010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010102011415010d00 -0a0109000e1d17081313170101010101010101010109001e010101010101010d1414041f1501010f0000140e0009011900120e01011c1c010603100801081600 -1601020101010101010201101210011b00000015070101040c041a011116001d1a001c0801011e1601011a131a1e000000150701010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101011800151d020d1e12181c121614110f0201010101010101010101110008010101020101011a -0013061a1801010418151501111d010d131700080a001a011b0000131916000014020701010507071e1f0c081e1b011f00121e001001011d1a040a0106160009 -1d1c0d0010011e000802131a08120019060701010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010105000f0c0c -01091b181a09120f0101071a0101010101010101011a000b01010101081e13131c0701010101010a021418010101010d1119121b001801050000160c00000019 -1a10000801121b02160000160f12040119121a161b0a010f100c161a011b16011818010017010e1b010d0c011800000501010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010201020004011f0410011f180b000d01011818010101010101010101010f050101010217120908 -080718001501010109000a010b1d081e1315090d00161c1314001e02000b070201031407010c000118001e00101013011a1307010e000c1c0a01190007180b01 -1e000a0700190115150004050000000f010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010119001901 -07180408010c00000d010f0801060601010101010101010101010101000c01020119001b0901010d161601011d030c0d010101011e1406010e000b010c1e0101 -0d000301011c1c02190000000f0a00011f140101011c001c010106000d170018011a13070d000a120000000000001b0101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101011a00131106011a151e07010d121e0801011908010201010101010101010101011f0a0101 -011f000d01011e001e0f010101030e0201011018100b1b010b001c01011e1e01060601010300030213000000091b001c001e0d16060d001d0a1801120c110a1b -0d01120d0a0000000f18001c04050101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010117180a -121901070c150a01010d120406080e040101010101010101010101010101010101020c1d01010e0e01010101040018020103001210011f07010f001101091901 -01010b16140c04140000000b01131c091513061b110400100a1601151f0101101e021216141217000a07000b0101020101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101020111000d06181c09010110010201010d1416000401010101010101020101010101010101 -0101010101010101010101010f0f0101010304010102010101011d18020102010101020f070219051a000402011511010d00020c000000080f00081503081701 -1e071800000f01161601110009010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101030000 -1b061010010101010101010101040014010101020101011e060101010101010101010101010108130b0101010101020101010201010101010101010101010101 -01010101010102010a000a01041308011b1708001c05140618000a14080214021e1c16031e180118160106050201010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101131e10190301010101010101020101011d141a0a0a0101020b1e01011a1d01010101 -010101010101010f001a0102010102010101010101010101010101010101010101010f121002171c12110b14001f061100031e1c01010702121814160105160f -141f171f1000050c0002010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020b00 -07011f030101010101010101010101010109091f0a0b160701061305010202010101010101010101191701010101010101010101010101020101010101010101 -010119120101021010010101090c14110c1c170101011d1b001615120a18001b1310071306040c17140102010101010101020101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101071315060101010101010101010101010201010102181c000a01101419081e020101 -01010101010201071605010101010101010101010101010101010c1a0101010101020d0008010118170101010101020101010101010a001b0f0f010e14141301 -0e120212051e00000401010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010119 -00160c0601020101010101010101010101010101010a041400001e121b0101010101080101060e1204010102010101010101010101060401011a000f01020101 -010101151c02010a160101010102010101010201010a0e010101010d001c140601121813160000131a0101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010f001415140a010101010101010101010101010101020101061d0c1b0e011d1303 -010f14151115150a010101011f150201010101011f120b01010e150101010101010102011c1c02010f0101051001010101010201010101020101011e04011c06 -0104121300140a060101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -1b0008021b0c01010101010101010101010101010101010101010101011d0408011a0b090207010102010101151501010101020109090101010e150101010101 -0102010101040b0101021c1309010206011c0c010c15010101010c1e011f190101041500161d0101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010f00131d0105111d010102010101010101010101010101010101010101010101 -011100110f010101010e14141b06010101010101010101010109130801010101011b0f0102011d06011a000901011115011417011d1d01010f1c1e0709131710 -01140016080101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010c00000401011919010101010101010101010101010101010101010101010101011d0b1f0101020110030101010101010101010101010102011f0401010101 -1d00080102010e1e011f15010101150e010d0d01010101011d0f0101170d001f0500190801010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101011613041b1a0109100f020101010101010101010101010101010101010101 -0101010101060102010b1b040e0e050701020101010101010101011e0d0101011c0b01020101080d0107151f01011b0b010101010101010101010102071f0013 -00160201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010f1308011f1909010f01010101010101010101010101010101010101010101010101071500171d091d030d0b11151e11181f1f0202010101010102010101 -0601011f1f010101010109121a01031306010109100f031516141414150016120017010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010104140201091e170801010e060101010101010101010101010101010101 -01010101010603191515110e1a070101061a041117041e1e0b0b0a060606020101010108080505010703011f1310061317100d00001200141007091400001213 -13070101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101020114150801020c120b0205090101010101010101010101010101010101010101010101010101021a04151514180c100201010101101a0b0c0404040404 -151618191200160f1b001c12131313121e1313040809001101070c00000000000401010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010900000c070106041205010101010101010101010101010101010101 -0101010101010201011002010102091a1113131b18051d07010101010101010613180a1400151916001c19050910130f071200040c160015111300001c101500 -11010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101011a000016030101081e0a010101010101010101010101010101010101010101010101010816150403020101010108030d1e121312151711180b1d0e -000f091500190e1c00181a1f1f1b001213121c0414000e06110011001c1800001a01010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010218001a11110f0101010101010101010101010102010101010101 -01010101010101010101091f111c17031a080101010101061010051904111200121313161416000017171e1c001c031c1c020201140b0103160016140000000a -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101011e1f010611151a0201010101010101010101010101010101010102010101010102020801010101060519191818190e1a06020101081f010e16 -011d13060109000d01010119000808001f06091c001e120016000301150015010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101011000150f010a14140c01010101010101010101010102010101 -01010101010101011d1312110d030701010101071d0e0b0b1b1b1b13130b15130e15001e1e1500141111160012161313161200150b14000c05001f1b00160801 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101101b131810011a14140301010101010106041a010101011c000b080101010101010603041c1c151511110b031001060101011c1908151403 -1616030316001f0f1f001c0203000501011713070714001116000000001a02010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010300001107010a150a01010101010715001e1a010110 -0e1b121715180d0d0f01010101010106100a0a040b150012190b000d081218011c0c010a001101011e000709131f01050400140413000000191600171a010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010a1200130e02010101010102010101090c16160410010101100d111c151212150d0a1a0101010101010103030a1100041500161616 -16131300161114000013160000121300000b1e00180500190d000008010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010108170000150507010101020101010102011d191b -1c191a020101010107100e0015151c0c0b10070105050101190f010c19010e15011a000b01010b140619000c090a1200180500001914001413001b0101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101011e0000000018070101010101010101010101081d0e1b1c0d19130201181b01010211120c1b1200141716130d1511010f001e -021816020701140b101300040b11000013001c000000001300120901010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201071a1e000000121908010101010102010101 -0101010216161e1e0e0400030101021e1d01051810100c0c1c00041713130000000014141314141500161914001c051100040500000000160a0e010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010a1600131217110a01010101010101010e0d09020101010e000b03050b121b01031702011002021c1a010500051014 -001a0209150e011f0c0110160c0109160013000013130e0201010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101081c001c030e1b150c0701010201 -010d17151e18030604030101031c1d0a1e13151916161f121c1a0b0004010a000301011e19010b000f0d00000d11001300120400001901020101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010201011a12000b0109191713190101010101011a0d0e181b18191b00181a170f0211141a0d13030e0000041b000000 -181b12001616001313001c1800000000000000000501010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -0101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010e13001902021004141c -0e0901010201010101060f0c181e1600151600111d150d01190004091b00050f03001e04000014130016141b1300000000161105010102010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101020108191200131f02080a111c1c05010101010101010101010101091f1d0400151c000012130000121612 -0000141414000016190b150000000000001001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101020d1500001c1f -06011d0d0101010107180c0c0b0a060201010104190104001a04000c090400001300131416001412141300001300001510010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010201010102080d1c0000150a1d0601010101080a051817170e181e1400171400041e001e0119000502 -02081d08121203160000000000000301010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010107 -1a150000001b03060201010101010101020f0f100b0b0b14121613131300040411171e1e13000000000000181a09010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101021a0b150000000014161b180509091009010101010101090e190c181c00 -000000000000160e110c1d0101010201010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01020101010110030b11121213000000000000141515161616000000000000001617181910100801010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010201010101010101010202030405060708090a0b0c0d0d0d0d0e0d050f -01010101010101010101020101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010001010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010102010101010101010101020101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101000101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010201010101010102010101020101010201020101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101 -01010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010101010100040000002701ffff030000000000}}}{\rtlch\fcs1 \af31507 \ltrch\fcs0 \charrsid4728168 -\par }\pard\plain \ltrpar\s20\qc \li864\ri864\sb200\sa160\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin864\lin864\itap0\pararsid4728168 \rtlch\fcs1 \ai\af31507\afs24\alang1025 \ltrch\fcs0 -\i\f31506\fs24\cf19\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af31507 \ltrch\fcs0 \charrsid4728168 -It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore u}{ -\rtlch\fcs1 \af31507 \ltrch\fcs0 nknown to shine amongst men.}{\rtlch\fcs1 \af31507 \ltrch\fcs0 \charrsid4728168 -\par }\pard\plain \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af31507 \ltrch\fcs0 -\page -\par }\pard\plain \ltrpar\s16\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168\contextualspace \rtlch\fcs1 \af31503\afs56\alang1025 \ltrch\fcs0 -\fs56\expnd-2\expndtw-10\lang1036\langfe1033\kerning28\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af31503 \ltrch\fcs0 \hich\af31502\dbch\af31501\loch\f31502 A paragraph -\par }\pard\plain \ltrpar\qj \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 -\af31507 \ltrch\fcs0 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut al -iquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 {\rtlch\fcs1 \af31507 \ltrch\fcs0 -\par }\pard\plain \ltrpar\s16\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168\contextualspace \rtlch\fcs1 \af31503\afs56\alang1025 \ltrch\fcs0 -\fs56\expnd-2\expndtw-10\lang1036\langfe1033\kerning28\loch\af31502\hich\af31502\dbch\af31501\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af31503 \ltrch\fcs0 \hich\af31502\dbch\af31501\loch\f31502 Another pa\hich\af31502\dbch\af31501\loch\f31502 ragrah - -\par }\pard\plain \ltrpar\qj \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 \rtlch\fcs1 \af31507\afs24\alang1025 \ltrch\fcs0 \f31506\fs24\lang1036\langfe1033\cgrid\langnp1036\langfenp1033 {\rtlch\fcs1 \af0 -\ltrch\fcs0 \f0\charrsid4728168 -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure d -olor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. -\par }\pard \ltrpar\ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0\pararsid4728168 {\rtlch\fcs1 \af31507 \ltrch\fcs0 \charrsid1669608 -\par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a -9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad -5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 -b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 -0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 -a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f -c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 -0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 -a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 -6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b -4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b -4757e8d3f729e245eb2b260a0238fd010000ffff0300504b030414000600080000002100f9627f54c60600008e1a0000160000007468656d652f7468656d652f -7468656d65312e786d6cec59dd8adb4614be2ff41d84ee1dff49fe59e20db66c6fdaec262176d2e672561e4b931d698c66bc1b13027d8242212dbd29f4ae85de -04da37e8bba4b4e943f4cc489667ec71f6872d84d2352cf2f83b67be39e7e83b23cddd7b2f13ea9ce38c1396f6dcfa9d9aebe034643392463df7e9745ce9b80e -17289d21ca52dc735798bbf70e3ffde42e3a10314eb003f6293f403d3716627150adf2108611bfc3163885dfe62c4b9080af59549d65e802fc26b4daa8d55ad5 -0491d475529480db69fcfbcfe0ecd17c4e42ec1eaebd8f284c910a2e07429a4da46f5c980c9619464b859d9dd52582af784033e71cd19e0b13cdd8c514bf14ae -431117f043cfada93fb77a78b78a0e0a232af6d86a7663f557d81506b3b3869a338b4ecb493dcff75afdd2bf0250b18b1bb547ad51abf4a700280c61a53917d3 -67bb1178055603e59716dfc3f6b05937f09affe60ee7be2f3f065e8172ffde0e7e3c0e208a065e8172bcbf83f707ddc1d0f4af4039beb5836fd7fa43af6df857 -a09892f46c075df35bcd60bdda123267f4be15def5bd71bb5138dfa0a01acaea9253cc592af6d55a825eb06c0c0009a44890d411ab059ea310ca3840949c66c4 -3926510c85b74029e3305c6bd4c6b526fc971f4f5da98ca2038c346bc90b98f09d21c9c7e1614616a2e77e0e5e5d0df27ce91c311193b0985539312ceea334d2 -2ddefff4cddf3f7ce5fcf5eb8fefdf7c9b4fba8de73a7e88d3e84b82d20f4d00abdd84e1dd776ffff8ededbbefbffef3973716fffd0c9deaf0294930771ee20b -e7094b60719615e0d3ec7a16d31811dda29f461ca548ce62f13f82f8e9e8872b449105378048e8b86719c88c0d78b47c61109ec4d952108bc7077162004f18a3 -039659a3f040cea58579ba4c23fbe4d952c73d41e8dc3677805223cfa3e502f495d85c063136683ea6281528c229168efc8d9d616c59dd73428cb89e9030639c -cd85f39c380344ac21999253a39a3646f749027959d90842be8dd89c3c73068cda563dc4e72612ee0e442de4a7981a613c424b81129bcb294aa81ef06324621b -c9c92a0b75dc880bc8748429734633ccb9cde65106ebd592fe0024c69ef613ba4a4c6426c899cde731624c470ed95910a36461c34e481aebd8cff8199428721e -3361839f30f30e91df210f201efbd2fd8c6023dd97abc15350579dd2a640e42fcbcc92cb23cc8cfa9dace81c61253520fe86a62724bd54e0b7a4ddfff7a4fd84 -a461cc2c2bba2d51b7bb3632724d39ef67c47a3fdddf12f17db86de90e5836231fbf720fd1327d8ce166d96d5fff0bf7ffc2edfee7857bdffd7cfb72bd516810 -6fb975cd37eb6aeb9eecddb9cf09a513b1a2f898abcd3b87be341bc3a0b4538fadb87c925bc47029ef6498c0c0451952364ec6c41744c493182d60875f77a593 -8817ae23ee2c18878dbf1ab6fa9678ba4c4ed82c7f60add7e5c3692e1e1c89cd78cd2fc7e16143e4e8567bf31056ba576c23f5b0bc26206daf43429bcc24d1b4 -9068af076590d4a33904cd4242adec5658742d2c3ad2fd3a553b2c805a9915d83839b0ddeab9be07266004cf5488e299cc539eea757655326f33d3fb82695400 -ec22d615b0c9745772ddbb3cb9babcd4ae90698384566e26091519d5c3788c66b8a84e397a151ad7cd75779352839e0c859a0f4a6b43a3ddf9108b9be61aecb6 -b581a6ba52d0d4b9e8b9ada60f2513a245cf9dc3833f5c260ba81d2e37bc8846f0fa2c14597ec3df445916191743c4e33ce04a7472354888c0994349d273e5f2 -cb34d0546988e2566f80207cb4e4ba202b1f1b3948ba99643c9fe350e869d74664a4f3afa0f0b956587f55e637074b4bb684744fe2d985734a97d9130425e6b7 -eb328033c2e1fd4f3d8fe68cc00bcd52c836f5b7d5980ad9d5df28aa1acac7115dc4a8e828ba98e77025e5251df5ad8c81f6ad583304540b49d1084f23d960f5 -a01addb4ec1a3987bd5df7722319394d34373dd35015d935ed2a66ccb06e035bb1bc5993d758ad430c9aa677f85cbab725b7bbd6baad7d42d92520e065fc2c5d -f70a0d41a3b699cca02619efcab0d4ec62d4ec1deb055e42ed2a4d4253fdd6daed56dcca1e619d0e066fd4f9c16ebb6a6168bede57aa48aba30ffd70829dbe00 -f118c26be025155ca5128e1e32041ba289da93e4b201b7c84b51dc1a70e52c33d2735fd5fcbe1734fca052ebf8a38ad7f46a958edf6f56fabedfac8ffc7a6d38 -68bc86c622e2a4eee7c72e6378114557c5e18b1adf398049d6efdaee842ca93275b25255c4d5014cbd611cc0e4272fce541eb0b80e01d179d56a8cbbcdeea055 -e936fbe38a371c742adda035a80c5b417b381e067ea73b7eed3ae70aecf59b81d71a752aad7a1054bc564dd2ef742b6dafd1e87bed7e67e4f55f17db1858792e -1f452c20bc8ad7e13f000000ffff0300504b0304140006000800000021000dd1909fb60000001b010000270000007468656d652f7468656d652f5f72656c732f -7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be -9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b060828e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980 -ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5b -babac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c0200001300000000000000000000000000000000005b436f6e74656e -745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000360100000b00000000000000000000000000300100005f72656c732f -2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c00000000000000000000000000190200007468656d652f7468656d652f74 -68656d654d616e616765722e786d6c504b01022d0014000600080000002100f9627f54c60600008e1a00001600000000000000000000000000d6020000746865 -6d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000dd1909fb60000001b0100002700000000000000000000000000d00900007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000cb0a00000000} -{\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d -617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 -6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 -656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} -{\*\latentstyles\lsdstimax371\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; -\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; -\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; -\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; -\lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Table;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 1; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Simple 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Classic 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Colorful 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 3; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Columns 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 6; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Grid 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 6; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table List 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table 3D effects 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Contemporary;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Elegant;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Professional; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Subtle 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 1;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 2; -\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Web 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Table Theme;\lsdsemihidden1 \lsdlocked0 Placeholder Text; -\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid;\lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2; -\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2;\lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List; -\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1;\lsdpriority61 \lsdlocked0 Light List Accent 1; -\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1;\lsdsemihidden1 \lsdlocked0 Revision; -\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1; -\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1;\lsdpriority72 \lsdlocked0 Colorful List Accent 1; -\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2; -\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2; -\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2;\lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2; -\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3; -\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3; -\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3;\lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4; -\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4; -\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4;\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4; -\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4;\lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5; -\lsdpriority62 \lsdlocked0 Light Grid Accent 5;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; -\lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; -\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; -\lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; -\lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; -\lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; -\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; -\lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; -\lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; -\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; -\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; -\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; -\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; -\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; -\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; -\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; -\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; -\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; -\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; -\lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; -\lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; -\lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; -\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; -\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; -\lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; -\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; -\lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; -\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; -\lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; -\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; -\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;}}{\*\datastore 010500000200000018000000 -4d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 -d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff -ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e50000000000000000000000006014 -a5e70e92d401feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 -000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 -0000000000000000000000000000000000000000000000000105000000000000}} \ No newline at end of file diff --git a/test/testdata/office/document.txt b/test/testdata/office/document.txt deleted file mode 100644 index 16eab9d7..00000000 --- a/test/testdata/office/document.txt +++ /dev/null @@ -1,3 +0,0 @@ -Gutenberg - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/office/document_with_special_éà.txt b/test/testdata/office/document_with_special_éà.txt deleted file mode 100644 index 16eab9d7..00000000 --- a/test/testdata/office/document_with_special_éà.txt +++ /dev/null @@ -1,3 +0,0 @@ -Gutenberg - -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. \ No newline at end of file diff --git a/test/testdata/pdf/gotenberg.pdf b/test/testdata/pdf/gotenberg.pdf deleted file mode 100644 index 0a0b284f..00000000 Binary files a/test/testdata/pdf/gotenberg.pdf and /dev/null differ diff --git a/test/testdata/pdf/gotenberg_bis.pdf b/test/testdata/pdf/gotenberg_bis.pdf deleted file mode 100644 index 0a0b284f..00000000 Binary files a/test/testdata/pdf/gotenberg_bis.pdf and /dev/null differ diff --git a/test/testdata/url/footer.html b/test/testdata/url/footer.html deleted file mode 100644 index 4fb22cdf..00000000 --- a/test/testdata/url/footer.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - -

- of -

- - \ No newline at end of file diff --git a/test/testdata/url/header.html b/test/testdata/url/header.html deleted file mode 100644 index abba6009..00000000 --- a/test/testdata/url/header.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/test/xerror.go b/test/xerror.go deleted file mode 100644 index 6bc04278..00000000 --- a/test/xerror.go +++ /dev/null @@ -1,18 +0,0 @@ -package test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/thecodingmachine/gotenberg/internal/pkg/xerror" -) - -// AssertError validates that given error -// is of an instance of xerror.Error. -// If so, returns the instance of xerror.Error. -func AssertError(t *testing.T, err error) *xerror.Error { - assert.NotNil(t, err) - standardized, ok := err.(*xerror.Error) - assert.Equal(t, true, ok) - return standardized -} diff --git a/test/xlog.go b/test/xlog.go deleted file mode 100644 index b8c93611..00000000 --- a/test/xlog.go +++ /dev/null @@ -1,23 +0,0 @@ -package test - -import ( - "github.com/thecodingmachine/gotenberg/internal/pkg/xlog" -) - -// DebugLogger creates a xlog.Logger -// with xlog.DebugLevel for our tests. -func DebugLogger() xlog.Logger { - return xlog.New(xlog.DebugLevel, "tests") -} - -// InfoLogger creates a xlog.Logger -// with xlog.InfoLevel for our tests. -func InfoLogger() xlog.Logger { - return xlog.New(xlog.DebugLevel, "tests") -} - -// ErrorLogger creates a xlog.Logger -// with xlog.ErrorLevel for our tests. -func ErrorLogger() xlog.Logger { - return xlog.New(xlog.ErrorLevel, "tests") -}