├── .cfnnag_global_suppress_list
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ ├── feature_request.md
│ └── general_question.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ ├── build.yml
│ └── upgrade-dependencies.yml
├── .gitignore
├── .viperlightignore
├── .viperlightrc
├── .vscode
└── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.txt
├── NOTICE.txt
├── README.md
├── architecture.png
├── buildspec.yml
├── deployment
├── build-s3-dist.sh
├── build-s3-dist2.sh
├── helper.py
└── run-unit-tests.sh
└── source
├── constructs
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── .npmignore
├── README.md
├── bin
│ └── constructs.ts
├── cdk.context.json
├── cdk.json
├── ecs-image-handler-arch.svg
├── images
│ ├── 01-edit-bucket-policy.png
│ └── 02-edit-bucket-policy.png
├── lambda-image-handler-arch.svg
├── lib
│ ├── api.json
│ ├── constructs-stack.ts
│ ├── ecs-image-handler.ts
│ ├── lambda-image-handler.ts
│ └── serverless-image-handler.ts
├── package.json
├── test
│ ├── __snapshots__
│ │ ├── constructs.test.ts.snap
│ │ └── ecs-image-handler.test.ts.snap
│ ├── constructs.test.ts
│ ├── ecs-image-handler.test.ts
│ └── serverless-image-handler-test.json
├── tsconfig.eslint.json
├── tsconfig.jest.json
├── tsconfig.json
└── yarn.lock
├── custom-resource
├── index.js
├── package.json
└── test
│ └── index.spec.js
├── demo-ui
├── index.html
├── scripts.js
└── style.css
├── image-handler
├── image-handler.js
├── image-request.js
├── index.js
├── package.json
├── test
│ ├── image-handler.spec.js
│ ├── image-request.spec.js
│ ├── image
│ │ └── test.jpg
│ ├── index.spec.js
│ └── thumbor-mapping.spec.js
└── thumbor-mapping.js
└── new-image-handler
├── .dockerignore
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── .npmignore
├── Dockerfile
├── Dockerfile.dev
├── Dockerfile.lambda
├── Dockerfile.lambda.deps
├── ImageMagick.Makefile
├── fonts
├── FangZhengHeiTi-GBK-1.ttf
└── README.md
├── package.json
├── src
├── config.ts
├── debug.ts
├── default.ts
├── imagemagick.ts
├── index-lambda.ts
├── index.ts
├── is.ts
├── processor
│ ├── image
│ │ ├── _base.ts
│ │ ├── auto-orient.ts
│ │ ├── blur.ts
│ │ ├── bright.ts
│ │ ├── cgif.ts
│ │ ├── circle.ts
│ │ ├── contrast.ts
│ │ ├── crop.ts
│ │ ├── format.ts
│ │ ├── grey.ts
│ │ ├── index.ts
│ │ ├── indexcrop.ts
│ │ ├── info.ts
│ │ ├── interlace.ts
│ │ ├── jpeg.js
│ │ ├── quality.ts
│ │ ├── resize.ts
│ │ ├── rotate.ts
│ │ ├── rounded-corners.ts
│ │ ├── sharpen.ts
│ │ ├── strip-metadata.ts
│ │ ├── threshold.ts
│ │ └── watermark.ts
│ ├── index.ts
│ ├── style.ts
│ └── video.ts
├── store.ts
├── style.json
└── typings.d.ts
├── test
├── bench
│ ├── README.md
│ ├── curl.sh
│ ├── perf.ts
│ ├── urls.txt
│ └── vegeta-urls.txt
├── default.test.ts
├── e2e
│ ├── README.md
│ └── mkhtml.py
├── fixtures
│ ├── .gitignore
│ ├── aws_logo.png
│ ├── example.gif
│ ├── example.jpg
│ ├── example.mp4
│ ├── f.jpg
│ └── gray-200x100.jpg
├── imagemagick.test.ts
├── index-lambda.test.ts
├── processor
│ ├── image
│ │ ├── _base.test.ts
│ │ ├── auto-orient.test.ts
│ │ ├── blur.test.ts
│ │ ├── bright.test.ts
│ │ ├── cgif.test.ts
│ │ ├── circle.test.ts
│ │ ├── contrast.test.ts
│ │ ├── crop.test.ts
│ │ ├── format.test.ts
│ │ ├── grey.test.ts
│ │ ├── indexcrop.test.ts
│ │ ├── info.test.ts
│ │ ├── interlace.test.ts
│ │ ├── quality.test.ts
│ │ ├── resize.test.ts
│ │ ├── rotate.test.ts
│ │ ├── rounded-corners.test.ts
│ │ ├── sharpen.test.ts
│ │ ├── threshold.test.ts
│ │ ├── utils.ts
│ │ └── watermark.test.ts
│ └── index.test.ts
└── store.test.ts
├── tsconfig.eslint.json
├── tsconfig.jest.json
├── tsconfig.json
└── yarn.lock
/.cfnnag_global_suppress_list:
--------------------------------------------------------------------------------
1 | # Instructions
2 | # ------------
3 | # 1) Add any cfn_nag rules that don't apply to this solution, providing a reason for each item
4 | # 2) Rename this file to .cfnnag_global_suppress_list
5 | # Reference: https://github.com/stelligent/cfn_nag#global-blacklist
6 | ---
7 | RulesToSuppress:
8 | - id: W89
9 | reason: Lambda functions in this solution do not need to be deployed in a VPC
10 | - id: W33
11 | reason: Your reason
12 | - id: W58
13 | reason: Your reason
14 | - id: W40
15 | reason: Your reason
16 | - id: W5
17 | reason: Your reason
18 | - id: W60
19 | reason: Your reason
20 | - id: W92
21 | reason: Your reason
22 |
23 | - id: W46
24 | reason: ApiGateway V2 should have access logging configured
25 | - id: W10
26 | reason: CloudFront Distribution should enable access logging
27 | - id: W70
28 | reason: Cloudfront should use minimum protocol version TLS 1.2
29 | - id: W78
30 | reason: DynamoDB table should have backup enabled, should be set using PointInTimeRecoveryEnabled
31 | - id: W74
32 | reason: DynamoDB table should have encryption enabled using a CMK stored in KMS
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 |
13 | **To Reproduce**
14 |
15 |
16 | **Expected behavior**
17 |
18 |
19 | **Please complete the following information about the solution:**
20 | - [ ] Version: [e.g. v1.0.0]
21 | - [ ] Region: [e.g. us-east-1]
22 | - [ ] Was the solution modified from the version published on this repository?
23 | - [ ] If the answer to the previous question was yes, are the changes available on GitHub?
24 | - [ ] Have you checked your [service quotas](https://docs.aws.amazon.com/general/latest/gr/aws_service_limits.html) for the sevices this solution uses?
25 | - [ ] Were there any errors in the CloudWatch Logs?
26 |
27 | **Screenshots**
28 |
29 |
30 | **Additional context**
31 |
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this solution
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 |
12 |
13 | **Describe the feature you'd like**
14 |
15 |
16 | **Additional context**
17 |
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/general_question.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: General question
3 | about: Ask a general question
4 | title: ''
5 | labels: question
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What is your question?**
11 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | **Issue #, if available:**
2 |
3 |
4 |
5 |
6 | **Description of changes:**
7 |
8 |
9 |
10 | **Checklist**
11 | - [ ] :wave: I have run the unit tests, and all unit tests have passed.
12 | - [ ] :warning: This pull request might incur a breaking change.
13 |
14 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
15 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | name: Build
4 | on:
5 | push:
6 | branches:
7 | - master
8 | pull_request: {}
9 | workflow_dispatch: {}
10 | jobs:
11 | build:
12 | runs-on: ubuntu-latest
13 | strategy:
14 | matrix:
15 | node: [ '14' ]
16 | name: Node ${{ matrix.node }}
17 | defaults:
18 | run:
19 | working-directory: source/constructs
20 | permissions:
21 | checks: write
22 | contents: write
23 | env:
24 | CI: "true"
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v2
28 | with:
29 | ref: ${{ github.event.pull_request.head.ref }}
30 | repository: ${{ github.event.pull_request.head.repo.full_name }}
31 | - name: Setup node
32 | uses: actions/setup-node@v2
33 | with:
34 | node-version: ${{ matrix.node }}
35 | - name: Install dependencies
36 | run: yarn install --check-files --frozen-lockfile
37 | - name: Test
38 | run: yarn test
39 | - name: CDK synth
40 | run: yarn synth
41 | - name: Build docker
42 | run: yarn build:new-image-handler:docker
43 |
--------------------------------------------------------------------------------
/.github/workflows/upgrade-dependencies.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | name: upgrade-dependencies
4 | on:
5 | workflow_dispatch: {}
6 | schedule:
7 | - cron: 0 0 * * *
8 | jobs:
9 | upgrade-new-image-handler:
10 | name: Upgrade
11 | runs-on: ubuntu-latest
12 | defaults:
13 | run:
14 | working-directory: source/new-image-handler
15 | permissions:
16 | contents: read
17 | outputs:
18 | conclusion: ${{ steps.build.outputs.conclusion }}
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v2
22 | - name: Install dependencies
23 | run: yarn install --check-files --frozen-lockfile
24 | - name: Upgrade dependencies
25 | run: yarn upgrade-dependencies
26 | - name: Build
27 | id: build
28 | run: yarn build && yarn build:new-image-handler:docker && echo "::set-output name=conclusion::success" || echo
29 | "::set-output name=conclusion::failure"
30 | - name: Create Patch
31 | run: |-
32 | git add .
33 | git diff --patch --staged > .upgrade.tmp.patch
34 | - name: Upload patch
35 | uses: actions/upload-artifact@v2
36 | with:
37 | name: .upgrade.tmp.patch
38 | path: source/new-image-handler/.upgrade.tmp.patch
39 | pr:
40 | name: Create Pull Request
41 | needs: upgrade-new-image-handler
42 | runs-on: ubuntu-latest
43 | defaults:
44 | run:
45 | working-directory: source/new-image-handler
46 | permissions:
47 | contents: write
48 | pull-requests: write
49 | checks: write
50 | steps:
51 | - name: Checkout
52 | uses: actions/checkout@v2
53 | - name: Download patch
54 | uses: actions/download-artifact@v2
55 | with:
56 | name: .upgrade.tmp.patch
57 | path: ${{ runner.temp }}
58 | - name: Apply patch
59 | run: '[ -s ${{ runner.temp }}/.upgrade.tmp.patch ] && git apply ${{ runner.temp
60 | }}/.upgrade.tmp.patch || echo "Empty patch. Skipping."'
61 | - name: Create Pull Request
62 | id: create-pr
63 | uses: peter-evans/create-pull-request@v3
64 | with:
65 | token: ${{ secrets.GITHUB_TOKEN }}
66 | commit-message: >-
67 | chore(deps): upgrade dependencies
68 |
69 |
70 | Upgrades project dependencies. See details in [workflow run].
71 |
72 |
73 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
74 |
75 |
76 | ------
77 |
78 |
79 | *Automatically created by projen via the "upgrade-dependencies" workflow*
80 | branch: github-actions/upgrade-dependencies
81 | title: "chore(deps): upgrade dependencies"
82 | body: >-
83 | Upgrades project dependencies. See details in [workflow run].
84 |
85 |
86 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
87 |
88 |
89 | ------
90 |
91 |
92 | *Automatically created by projen via the "upgrade-dependencies" workflow*
93 | - name: Update status check
94 | if: steps.create-pr.outputs.pull-request-url != ''
95 | run: "curl -i --fail -X POST -H \"Accept: application/vnd.github.v3+json\" -H
96 | \"Authorization: token ${GITHUB_TOKEN}\"
97 | https://api.github.com/repos/${{ github.repository }}/check-runs -d
98 | '{\"name\":\"build\",\"head_sha\":\"github-actions/upgrade-dependenci\
99 | es\",\"status\":\"completed\",\"conclusion\":\"${{
100 | needs.upgrade.outputs.conclusion }}\",\"output\":{\"title\":\"Created
101 | via the upgrade-dependencies workflow.\",\"summary\":\"Action run URL:
102 | https://github.com/${{ github.repository }}/actions/runs/${{
103 | github.run_id }}\"}}'"
104 | env:
105 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
106 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | **/dist
3 | **/global-s3-assets
4 | **/regional-s3-assets
5 | **/open-source
6 | **/.zip
7 | **/tmp
8 | **/out-tsc
9 |
10 | # dependencies
11 | **/node_modules
12 |
13 | # test assets
14 | **/coverage
15 | **/.nyc_output
16 |
17 | # misc
18 | **/npm-debug.log
19 | **/testem.log
20 | **/package-lock.json
21 | **/.vscode/settings.json
22 | demo-ui-config.js
23 |
24 | # System Files
25 | **/.DS_Store
26 |
--------------------------------------------------------------------------------
/.viperlightignore:
--------------------------------------------------------------------------------
1 | # Ignore Config used with code.amazon.com
2 | Config
3 |
4 | # Use of opensource-codeofconduct email for amazon is expected
5 | CODE_OF_CONDUCT.md
6 | CONTRIBUTING.md
7 | README.md
8 | docs/en/index.md
9 | docs/zh/index.md
10 |
11 | cdk.out/
12 | ^dist/
13 | node_modules/
14 | package-lock.json
15 | yarn.lock
16 |
--------------------------------------------------------------------------------
/.viperlightrc:
--------------------------------------------------------------------------------
1 | {
2 | "all": true,
3 | "failOn": "medium"
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.format.enable": true,
3 | "eslint.lintTask.enable": true,
4 | "eslint.workingDirectories": [
5 | "source/constructs",
6 | "source/new-image-handler",
7 | ],
8 | "yaml.customTags": [
9 | "!And",
10 | "!And sequence",
11 | "!If",
12 | "!If sequence",
13 | "!Not",
14 | "!Not sequence",
15 | "!Equals",
16 | "!Equals sequence",
17 | "!Or",
18 | "!Or sequence",
19 | "!FindInMap",
20 | "!FindInMap sequence",
21 | "!Base64",
22 | "!Join",
23 | "!Join sequence",
24 | "!Cidr",
25 | "!Ref",
26 | "!Sub",
27 | "!Sub sequence",
28 | "!GetAtt",
29 | "!GetAZs",
30 | "!ImportValue",
31 | "!ImportValue sequence",
32 | "!Select",
33 | "!Select sequence",
34 | "!Split",
35 | "!Split sequence"
36 | ],
37 | }
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
3 | documentation, we greatly value feedback and contributions from our community.
4 |
5 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
6 | information to effectively respond to your bug report or contribution.
7 |
8 | ## Reporting Bugs/Feature Requests
9 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
10 |
11 | When filing an issue, please check [existing open](https://github.com/awslabs/serverless-image-handler/issues), or [recently closed](https://github.com/awslabs/serverless-image-handler/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already
12 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
13 |
14 | * A reproducible test case or series of steps
15 | * The version of our code being used
16 | * Any modifications you've made relevant to the bug
17 | * Anything unusual about your environment or deployment
18 |
19 | ## Contributing via Pull Requests
20 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
21 |
22 | 1. You are working against the latest source on the *master* branch.
23 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
24 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
25 |
26 | To send us a pull request, please:
27 |
28 | 1. Fork the repository.
29 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
30 | 3. Ensure local tests pass.
31 | 4. Commit to your fork using clear commit messages.
32 | 5. Send us a pull request, answering any default questions in the pull request interface.
33 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
34 |
35 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
36 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
37 |
38 | ## Finding contributions to work on
39 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels ((enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/awslabs/serverless-image-handler/labels/help%20wanted) issues is a great place to start.
40 |
41 | ## Code of Conduct
42 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
43 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
44 | opensource-codeofconduct@amazon.com with any additional questions or comments.
45 |
46 | ## Security issue notifications
47 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
48 |
49 | ## Licensing
50 | See the [LICENSE](https://github.com/awslabs/serverless-image-handler/blob/master/LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
51 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
52 |
--------------------------------------------------------------------------------
/NOTICE.txt:
--------------------------------------------------------------------------------
1 | Serverless Image Handler
2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 |
4 | **********************
5 | THIRD PARTY COMPONENTS
6 | **********************
7 | This software includes third party software subject to the following copyrights:
8 |
9 | aws-sdk under the Apache License Version 2.0
10 | axios under the Massachusetts Institute of Technology (MIT) license
11 | axios-mock-adapter under the Massachusetts Institute of Technology (MIT) license
12 | bootstrap under the Massachusetts Institute of Technology (MIT) license
13 | color under the Massachusetts Institute of Technology (MIT) license
14 | color-name under the Massachusetts Institute of Technology (MIT) license
15 | jest under the Massachusetts Institute of Technology (MIT) license
16 | mocha under the Massachusetts Institute of Technology (MIT) license
17 | sharp under the Apache License Version 2.0
18 | uuid under the Massachusetts Institute of Technology (MIT) license
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **_Important Notice:_**
2 | Due to a [change in the AWS Lambda execution environment](https://aws.amazon.com/blogs/compute/upcoming-updates-to-the-aws-lambda-execution-environment/), Serverless Image Handler v3 deployments are functionally broken. To address the issue we have released [minor version update v3.1.1](https://solutions-reference.s3.amazonaws.com/serverless-image-handler/v3.1.1/serverless-image-handler.template). We recommend all users of v3 to run cloudformation stack update with v3.1.1. Additionally, we suggest you to look at v5 of the solution and migrate to v5 if it addresses all of your use cases.
3 |
4 | # AWS Serverless Image Handler Lambda wrapper for SharpJS
5 | A solution to dynamically handle images on the fly, utilizing Sharp (https://sharp.pixelplumbing.com/en/stable/).
6 | Published version, additional details and documentation are available here: https://aws.amazon.com/solutions/serverless-image-handler/
7 |
8 | _Note:_ it is recommended to build the application binary on Amazon Linux.
9 |
10 | ## On This Page
11 | - [Architecture Overview](#architecture-overview)
12 | - [Creating a custom build](#creating-a-custom-build)
13 | - [External Contributors](#external-contributors)
14 | - [License](#license)
15 |
16 | ## Architecture Overview
17 | 
18 |
19 | The AWS CloudFormation template deploys an Amazon CloudFront distribution, Amazon API Gateway REST API, and an AWS Lambda function. Amazon CloudFront provides a caching layer to reduce the cost of image processing and the latency of subsequent image delivery. The Amazon API Gateway provides endpoint resources and triggers the AWS Lambda function. The AWS Lambda function retrieves the image from the customer's Amazon Simple Storage Service (Amazon S3) bucket and uses Sharp to return a modified version of the image to the API Gateway. Additionally, the solution generates a CloudFront domain name that provides cached access to the image handler API.
20 |
21 | _**Note**:_ From v5.0, all AWS CloudFormation template resources are created be [AWS CDK](https://aws.amazon.com/cdk/) and [AWS Solutions Constructs](https://aws.amazon.com/solutions/constructs/). Since the AWS CloudFormation template resources have the same logical ID comparing to v4.x, it makes the solution upgradable mostly from v4.x to v5.
22 |
23 | ## Creating a custom build
24 | The solution can be deployed through the CloudFormation template available on the solution home page.
25 | To make changes to the solution, download or clone this repo, update the source code and then run the deployment/build-s3-dist.sh script to deploy the updated Lambda code to an Amazon S3 bucket in your account.
26 |
27 | ### Prerequisites:
28 | * [AWS Command Line Interface](https://aws.amazon.com/cli/)
29 | * Node.js 12.x or later
30 |
31 | ### 1. Clone the repository
32 | ```bash
33 | git clone https://github.com/awslabs/serverless-image-handler.git
34 | ```
35 |
36 | ### 2. Run unit tests for customization
37 | Run unit tests to make sure added customization passes the tests:
38 | ```bash
39 | cd ./deployment
40 | chmod +x ./run-unit-tests.sh
41 | ./run-unit-tests.sh
42 | ```
43 |
44 | ### 3. Declare environment variables
45 | ```bash
46 | export REGION=aws-region-code # the AWS region to launch the solution (e.g. us-east-1)
47 | export DIST_OUTPUT_BUCKET=my-bucket-name # bucket where customized code will reside
48 | export SOLUTION_NAME=my-solution-name # the solution name
49 | export VERSION=my-version # version number for the customized code
50 | ```
51 |
52 | ### 4. Create an Amazon S3 Bucket
53 | The CloudFormation template is configured to pull the Lambda deployment packages from Amazon S3 bucket in the region the template is being launched in. Create a bucket in the desired region with the region name appended to the name of the bucket.
54 | ```bash
55 | aws s3 mb s3://$DIST_OUTPUT_BUCKET-$REGION --region $REGION
56 | ```
57 |
58 | ### 5. Create the deployment packages
59 | Build the distributable:
60 | ```bash
61 | chmod +x ./build-s3-dist.sh
62 | ./build-s3-dist.sh $DIST_OUTPUT_BUCKET $SOLUTION_NAME $VERSION
63 | ```
64 |
65 | Deploy the distributable to the Amazon S3 bucket in your account:
66 | ```bash
67 | aws s3 sync ./regional-s3-assets/ s3://$DIST_OUTPUT_BUCKET-$REGION/$SOLUTION_NAME/$VERSION/ --acl bucket-owner-full-control
68 | aws s3 sync ./global-s3-assets/ s3://$DIST_OUTPUT_BUCKET-$REGION/$SOLUTION_NAME/$VERSION/ --acl bucket-owner-full-control
69 | ```
70 |
71 | ### 6. Launch the CloudFormation template.
72 | * Get the link of the `serverless-image-handler.template` uploaded to your Amazon S3 bucket.
73 | * Deploy the Serverless Image Handler solution to your account by launching a new AWS CloudFormation stack using the S3 link of the `serverless-image-handler.template`.
74 |
75 | ## External Contributors
76 | - [@leviwilson](https://github.com/leviwilson) for [#117](https://github.com/awslabs/serverless-image-handler/pull/117)
77 | - [@rpong](https://github.com/rpong) for [#130](https://github.com/awslabs/serverless-image-handler/pull/130)
78 | - [@harriswong](https://github.com/harriswong) for [#138](https://github.com/awslabs/serverless-image-handler/pull/138)
79 | - [@ganey](https://github.com/ganey) for [#139](https://github.com/awslabs/serverless-image-handler/pull/139)
80 | - [@browniebroke](https://github.com/browniebroke) for [#151](https://github.com/awslabs/serverless-image-handler/pull/151), [#152](https://github.com/awslabs/serverless-image-handler/pull/152)
81 | - [@john-shaffer](https://github.com/john-shaffer) for [#158](https://github.com/awslabs/serverless-image-handler/pull/158)
82 | - [@toredash](https://github.com/toredash) for [#174](https://github.com/awslabs/serverless-image-handler/pull/174), [#195](https://github.com/awslabs/serverless-image-handler/pull/195)
83 | - [@lith-imad](https://github.com/lith-imad) for [#194](https://github.com/awslabs/serverless-image-handler/pull/194)
84 | - [@pch](https://github.com/pch) for [#227](https://github.com/awslabs/serverless-image-handler/pull/227)
85 | - [@atrope](https://github.com/atrope) for [#201](https://github.com/awslabs/serverless-image-handler/pull/201)
86 | - [@bretto36](https://github.com/bretto36) for [#182](https://github.com/awslabs/serverless-image-handler/pull/182)
87 | - [@makoncline](https://github.com/makoncline) for [#255](https://github.com/awslabs/serverless-image-handler/pull/255)
88 |
89 | ***
90 | ## License
91 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
92 | SPDX-License-Identifier: Apache-2.0
93 |
--------------------------------------------------------------------------------
/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/architecture.png
--------------------------------------------------------------------------------
/buildspec.yml:
--------------------------------------------------------------------------------
1 | version: 0.2
2 |
3 | phases:
4 | install:
5 | runtime-versions:
6 | nodejs: 12
7 | build:
8 | commands:
9 | - ./deployment/build-s3-dist2.sh ${DIST_OUTPUT_BUCKET} ${SOLUTION_NAME} ${VERSION}
10 | - mkdir -p deployment/open-source/ && touch deployment/open-source/.empty
11 | post_build:
12 | commands:
13 | - aws s3 cp s3://solutions-build-assets/changelog-spec.yml buildspec.yml
14 | artifacts:
15 | files:
16 | - .git/**/*
17 | - deployment/**/*
18 | - buildspec.yml
19 | - CHANGELOG.md
20 | - .cfnnag_*
21 |
--------------------------------------------------------------------------------
/deployment/build-s3-dist.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # This assumes all of the OS-level configuration has been completed and git repo has already been cloned
4 | #
5 | # This script should be run from the repo's deployment directory
6 | # cd deployment
7 | # ./build-s3-dist.sh source-bucket-base-name trademarked-solution-name version-code
8 | #
9 | # Paramenters:
10 | # - source-bucket-base-name: Name for the S3 bucket location where the template will source the Lambda
11 | # code from. The template will append '-[region_name]' to this bucket name.
12 | # For example: ./build-s3-dist.sh solutions my-solution v1.0.0
13 | # The template will then expect the source code to be located in the solutions-[region_name] bucket
14 | #
15 | # - trademarked-solution-name: name of the solution for consistency
16 | #
17 | # - version-code: version of the package
18 |
19 | # Check to see if input has been provided:
20 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
21 | echo "Please provide the base source bucket name, trademark approved solution name and version where the lambda code will eventually reside."
22 | echo "For example: ./build-s3-dist.sh solutions trademarked-solution-name v1.0.0"
23 | exit 1
24 | fi
25 |
26 | set -e
27 |
28 | # Get reference for all important folders
29 | template_dir="$PWD"
30 | template_dist_dir="$template_dir/global-s3-assets"
31 | build_dist_dir="$template_dir/regional-s3-assets"
32 | source_dir="$template_dir/../source"
33 |
34 | echo "------------------------------------------------------------------------------"
35 | echo "Rebuild distribution"
36 | echo "------------------------------------------------------------------------------"
37 | rm -rf $template_dist_dir
38 | mkdir -p $template_dist_dir
39 | rm -rf $build_dist_dir
40 | mkdir -p $build_dist_dir
41 |
42 | echo "------------------------------------------------------------------------------"
43 | echo "CloudFormation template with CDK and Constructs"
44 | echo "------------------------------------------------------------------------------"
45 | export BUCKET_NAME=$1
46 | export SOLUTION_NAME=$2
47 | export VERSION=$3
48 |
49 | cd $source_dir/constructs
50 | npm install
51 | npm run build && cdk synth --asset-metadata false --path-metadata false --json true > serverless-image-handler.json
52 | mv serverless-image-handler.json $template_dist_dir/serverless-image-handler.template
53 |
54 | echo "------------------------------------------------------------------------------"
55 | echo "Package the image-handler code"
56 | echo "------------------------------------------------------------------------------"
57 | cd $source_dir/image-handler
58 | npm install
59 | npm run build
60 | cp dist/image-handler.zip $build_dist_dir/image-handler.zip
61 |
62 | echo "------------------------------------------------------------------------------"
63 | echo "Package the demo-ui assets"
64 | echo "------------------------------------------------------------------------------"
65 | mkdir $build_dist_dir/demo-ui/
66 | cp -r $source_dir/demo-ui/** $build_dist_dir/demo-ui/
67 |
68 | echo "------------------------------------------------------------------------------"
69 | echo "Package the custom-resource code"
70 | echo "------------------------------------------------------------------------------"
71 | cd $source_dir/custom-resource
72 | npm install
73 | npm run build
74 | cp dist/custom-resource.zip $build_dist_dir/custom-resource.zip
75 |
76 | echo "------------------------------------------------------------------------------"
77 | echo "Generate the demo-ui manifest document"
78 | echo "------------------------------------------------------------------------------"
79 | cd $source_dir/demo-ui
80 | manifest=(`find * -type f ! -iname ".DS_Store"`)
81 | manifest_json=$(IFS=,;printf "%s" "${manifest[*]}")
82 | echo "{\"files\":[\"$manifest_json\"]}" | sed 's/,/","/g' >> $build_dist_dir/demo-ui-manifest.json
83 |
--------------------------------------------------------------------------------
/deployment/build-s3-dist2.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | title() {
5 | echo "------------------------------------------------------------------------------"
6 | echo $*
7 | echo "------------------------------------------------------------------------------"
8 | }
9 |
10 | run() {
11 | >&2 echo "[run] $*"
12 | $*
13 | }
14 |
15 | __dir="$(cd "$(dirname $0)";pwd)"
16 | SRC_PATH="${__dir}/../source"
17 | CDK_OUT_PATH="${__dir}/cdk.out"
18 |
19 | if [ -z "$1" ] || [ -z "$2" ]; then
20 | echo "Parameters not enough"
21 | echo "Example: $(basename $0) [VERSION]"
22 | exit 1
23 | fi
24 |
25 | export BUCKET_NAME=$1
26 | export SOLUTION_NAME=$2
27 | if [ -z "$3" ]; then
28 | # export VERSION="v$(jq -r '.version' ${SRC_PATH}/version.json)"
29 | export VERSION=$(git describe --tags || echo latest)
30 | else
31 | export VERSION=$3
32 | fi
33 | export GLOBAL_S3_ASSETS_PATH="${__dir}/global-s3-assets"
34 | export REGIONAL_S3_ASSETS_PATH="${__dir}/regional-s3-assets"
35 |
36 | title "init env"
37 |
38 | run rm -rf ${GLOBAL_S3_ASSETS_PATH} && run mkdir -p ${GLOBAL_S3_ASSETS_PATH}
39 | run rm -rf ${REGIONAL_S3_ASSETS_PATH} && run mkdir -p ${REGIONAL_S3_ASSETS_PATH}
40 | run rm -rf ${CDK_OUT_PATH}
41 |
42 | echo "BUCKET_NAME=${BUCKET_NAME}"
43 | echo "SOLUTION_NAME=${SOLUTION_NAME}"
44 | echo "VERSION=${VERSION}"
45 | echo "${VERSION}" > ${GLOBAL_S3_ASSETS_PATH}/version
46 |
47 | title "cdk synth"
48 |
49 | run cd ${SRC_PATH}/constructs
50 | run yarn
51 |
52 | export USE_BSS=true
53 | export BSS_TEMPLATE_BUCKET_NAME="${BUCKET_NAME}"
54 | export BSS_FILE_ASSET_BUCKET_NAME="${BUCKET_NAME}-\${AWS::Region}"
55 | export BSS_FILE_ASSET_PREFIX="${SOLUTION_NAME}/${VERSION}/"
56 | export BSS_FILE_ASSET_REGION_SET="us-east-1,${BSS_FILE_ASSET_REGION_SET}"
57 |
58 | run npm run synth -- --output ${CDK_OUT_PATH}
59 | run ${__dir}/helper.py ${CDK_OUT_PATH}
60 |
61 | title "tips!"
62 |
63 | echo "To test your cloudformation template"
64 | echo "make sure you have the following bucket exists in your account"
65 | echo " - ${BUCKET_NAME}"
66 | echo ${BSS_FILE_ASSET_REGION_SET} | tr ',' '\n' | xargs -I {} echo " - ${BUCKET_NAME}-{}"
67 | echo "run \`aws s3 cp --recursive ${GLOBAL_S3_ASSETS_PATH} s3://${BUCKET_NAME}/${SOLUTION_NAME}/${VERSION}\`"
68 | echo "run \`echo \"${BSS_FILE_ASSET_REGION_SET}\" | tr ',' '\n' | xargs -t -I {} aws s3 cp --recursive --region {} ${REGIONAL_S3_ASSETS_PATH} s3://${BUCKET_NAME}-{}/${SOLUTION_NAME}/${VERSION}\`"
69 |
--------------------------------------------------------------------------------
/deployment/helper.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import os
4 | import sys
5 | import json
6 | import glob
7 | import subprocess
8 |
9 |
10 | GLOBAL_S3_ASSETS_PATH = os.environ['GLOBAL_S3_ASSETS_PATH']
11 | REGIONAL_S3_ASSETS_PATH = os.environ['REGIONAL_S3_ASSETS_PATH']
12 |
13 |
14 | class Color(object):
15 | ISATTY = os.isatty(1)
16 | COLORS = {
17 | 'red': '\x1b[31m',
18 | 'green': '\x1b[32m',
19 | 'yellow': '\x1b[33m',
20 | 'blue': '\x1b[34m',
21 | 'reset': '\x1b[0m'
22 | }
23 |
24 | @staticmethod
25 | def c(s, code):
26 | if Color.ISATTY:
27 | return Color.COLORS[code] + s + Color.COLORS['reset']
28 | return s
29 |
30 | @staticmethod
31 | def red(s):
32 | return Color.c(s, 'red')
33 |
34 | @staticmethod
35 | def green(s):
36 | return Color.c(s, 'green')
37 |
38 | @staticmethod
39 | def yellow(s):
40 | return Color.c(s, 'yellow')
41 |
42 | @staticmethod
43 | def blue(s):
44 | return Color.c(s, 'blue')
45 |
46 |
47 | def get_file_assets(filename):
48 | with open(filename, 'r') as fp:
49 | assets = json.load(fp)
50 | files = assets['files']
51 |
52 | def _add_key(k, v):
53 | v['_id'] = k
54 | return v
55 |
56 | return [_add_key(k, v) for k, v in files.items()]
57 |
58 |
59 | def sh(*args):
60 | return subprocess.call(*args, shell=True)
61 |
62 |
63 | def zip(src, dst):
64 | print(f'{Color.yellow("[zip]")} {Color.green(f"{src} => {dst}")}')
65 | sh(f'cd {src} && zip -r {dst} .')
66 |
67 |
68 | def cp(src, dst):
69 | print(f'{Color.yellow("[cp]")} {Color.green(f"{src} => {dst}")}')
70 | sh(f'cp {src} {dst}')
71 |
72 |
73 | def main():
74 | dir_in = os.path.abspath(sys.argv[1])
75 | assets = glob.glob(os.path.join(dir_in, '*.assets.json'))
76 |
77 | for asset in assets:
78 | print(f'from {Color.blue(asset)}')
79 | file_assets = get_file_assets(asset)
80 | for file in file_assets:
81 | source = file['source']
82 | src = os.path.join(dir_in, source['path'])
83 | if src.endswith('template.json'):
84 | dst = os.path.abspath(os.path.join(GLOBAL_S3_ASSETS_PATH, file['_id'].replace('.json', '')))
85 | else:
86 | dst = os.path.abspath(os.path.join(REGIONAL_S3_ASSETS_PATH, file['_id']))
87 | if source['packaging'] == 'zip':
88 | zip(src, dst)
89 | elif source['packaging'] == 'file':
90 | cp(src, dst)
91 |
92 |
93 | if __name__ == '__main__':
94 | main()
95 |
--------------------------------------------------------------------------------
/deployment/run-unit-tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | current_dir=$PWD
6 | source_dir=$current_dir/../source
7 |
8 | cd $source_dir/constructs
9 | npm install
10 | npm test
11 |
12 | cd $source_dir/image-handler
13 | npm test
14 |
15 | cd $source_dir/custom-resource
16 | npm test
--------------------------------------------------------------------------------
/source/constructs/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true,
4 | "node": true
5 | },
6 | "root": true,
7 | "plugins": [
8 | "@typescript-eslint",
9 | "import"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 2018,
14 | "sourceType": "module",
15 | "project": "./tsconfig.eslint.json"
16 | },
17 | "extends": [
18 | "plugin:import/typescript"
19 | ],
20 | "settings": {
21 | "import/parsers": {
22 | "@typescript-eslint/parser": [
23 | ".ts",
24 | ".tsx"
25 | ]
26 | },
27 | "import/resolver": {
28 | "node": {},
29 | "typescript": {
30 | "project": "./tsconfig.eslint.json"
31 | }
32 | }
33 | },
34 | "ignorePatterns": [
35 | "*.js",
36 | "!.projenrc.js",
37 | "*.d.ts",
38 | "node_modules/",
39 | "*.generated.ts",
40 | "coverage"
41 | ],
42 | "rules": {
43 | "indent": [
44 | "off"
45 | ],
46 | "@typescript-eslint/indent": [
47 | "error",
48 | 2
49 | ],
50 | "quotes": [
51 | "error",
52 | "single",
53 | {
54 | "avoidEscape": true
55 | }
56 | ],
57 | "comma-dangle": [
58 | "error",
59 | "always-multiline"
60 | ],
61 | "comma-spacing": [
62 | "error",
63 | {
64 | "before": false,
65 | "after": true
66 | }
67 | ],
68 | "no-multi-spaces": [
69 | "error",
70 | {
71 | "ignoreEOLComments": false
72 | }
73 | ],
74 | "array-bracket-spacing": [
75 | "error",
76 | "never"
77 | ],
78 | "array-bracket-newline": [
79 | "error",
80 | "consistent"
81 | ],
82 | "object-curly-spacing": [
83 | "error",
84 | "always"
85 | ],
86 | "object-curly-newline": [
87 | "error",
88 | {
89 | "multiline": true,
90 | "consistent": true
91 | }
92 | ],
93 | "object-property-newline": [
94 | "error",
95 | {
96 | "allowAllPropertiesOnSameLine": true
97 | }
98 | ],
99 | "keyword-spacing": [
100 | "error"
101 | ],
102 | "brace-style": [
103 | "error",
104 | "1tbs",
105 | {
106 | "allowSingleLine": true
107 | }
108 | ],
109 | "space-before-blocks": [
110 | "error"
111 | ],
112 | "curly": [
113 | "error",
114 | "multi-line",
115 | "consistent"
116 | ],
117 | "@typescript-eslint/member-delimiter-style": [
118 | "error"
119 | ],
120 | "semi": [
121 | "error",
122 | "always"
123 | ],
124 | "max-len": [
125 | "error",
126 | {
127 | "code": 150,
128 | "ignoreUrls": true,
129 | "ignoreStrings": true,
130 | "ignoreTemplateLiterals": true,
131 | "ignoreComments": true,
132 | "ignoreRegExpLiterals": true
133 | }
134 | ],
135 | "quote-props": [
136 | "error",
137 | "consistent-as-needed"
138 | ],
139 | "@typescript-eslint/no-require-imports": [
140 | "error"
141 | ],
142 | "import/no-extraneous-dependencies": [
143 | "error",
144 | {
145 | "devDependencies": [
146 | "**/test/**",
147 | "**/build-tools/**"
148 | ],
149 | "optionalDependencies": false,
150 | "peerDependencies": true
151 | }
152 | ],
153 | "import/no-unresolved": [
154 | "error"
155 | ],
156 | "import/order": [
157 | "warn",
158 | {
159 | "groups": [
160 | "builtin",
161 | "external"
162 | ],
163 | "alphabetize": {
164 | "order": "asc",
165 | "caseInsensitive": true
166 | }
167 | }
168 | ],
169 | "no-duplicate-imports": [
170 | "error"
171 | ],
172 | "no-shadow": [
173 | "off"
174 | ],
175 | "@typescript-eslint/no-shadow": [
176 | "error"
177 | ],
178 | "key-spacing": [
179 | "error"
180 | ],
181 | "no-multiple-empty-lines": [
182 | "error"
183 | ],
184 | "@typescript-eslint/no-floating-promises": [
185 | "error"
186 | ],
187 | "no-return-await": [
188 | "off"
189 | ],
190 | "@typescript-eslint/return-await": [
191 | "error"
192 | ],
193 | "no-trailing-spaces": [
194 | "error"
195 | ],
196 | "dot-notation": [
197 | "error"
198 | ],
199 | "no-bitwise": [
200 | "error"
201 | ],
202 | "@typescript-eslint/member-ordering": [
203 | "error",
204 | {
205 | "default": [
206 | "public-static-field",
207 | "public-static-method",
208 | "protected-static-field",
209 | "protected-static-method",
210 | "private-static-field",
211 | "private-static-method",
212 | "field",
213 | "constructor",
214 | "method"
215 | ]
216 | }
217 | ]
218 | },
219 | "overrides": [
220 | {
221 | "files": [
222 | ".projenrc.js"
223 | ],
224 | "rules": {
225 | "@typescript-eslint/no-require-imports": "off",
226 | "import/no-extraneous-dependencies": "off"
227 | }
228 | }
229 | ]
230 | }
231 |
--------------------------------------------------------------------------------
/source/constructs/.gitattributes:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | *.snap linguist-generated
4 | /.eslintrc.json linguist-generated
5 | /.gitattributes linguist-generated
6 | /.github/pull_request_template.md linguist-generated
7 | /.github/workflows/build.yml linguist-generated
8 | /.github/workflows/upgrade-dependencies.yml linguist-generated
9 | /.gitignore linguist-generated
10 | /.mergify.yml linguist-generated
11 | /.npmignore linguist-generated
12 | /.projen/** linguist-generated
13 | /.projen/deps.json linguist-generated
14 | /.projen/tasks.json linguist-generated
15 | /cdk.json linguist-generated
16 | /LICENSE linguist-generated
17 | /package.json linguist-generated
18 | /tsconfig.eslint.json linguist-generated
19 | /tsconfig.jest.json linguist-generated
20 | /tsconfig.json linguist-generated
21 | /yarn.lock linguist-generated
--------------------------------------------------------------------------------
/source/constructs/.gitignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | *.lcov
3 | *.log
4 | *.pid
5 | *.pid.lock
6 | *.seed
7 | *.tgz
8 | *.tsbuildinfo
9 | .cache
10 | .cdk.staging/
11 | .eslintcache
12 | .nyc_output
13 | .parcel-cache/
14 | .yarn-integrity
15 | /coverage
16 | /dist
17 | /test-reports/
18 | build/Release
19 | cdk.out/
20 | coverage
21 | jspm_packages/
22 | junit.xml
23 | lerna-debug.log*
24 | lib-cov
25 | logs
26 | node_modules/
27 | npm-debug.log*
28 | pids
29 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
30 | yarn-debug.log*
31 | yarn-error.log*
32 | !/.eslintrc.json
33 | !/.gitattributes
34 | !/.github/pull_request_template.md
35 | !/.github/workflows/build.yml
36 | !/.github/workflows/upgrade-dependencies.yml
37 | !/.mergify.yml
38 | !/.npmignore
39 | !/.projen/deps.json
40 | !/.projen/tasks.json
41 | !/.projenrc.js
42 | !/LICENSE
43 | !/cdk.json
44 | !/package.json
45 | !/src
46 | !/test
47 | !/tsconfig.eslint.json
48 | !/tsconfig.jest.json
49 | !/tsconfig.json
50 |
--------------------------------------------------------------------------------
/source/constructs/.npmignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | .cdk.staging/
3 | /.eslintrc.json
4 | /.github
5 | /.idea
6 | /.mergify.yml
7 | /.projen
8 | /.projenrc.js
9 | /.vscode
10 | /coverage
11 | /src
12 | /test
13 | /test-reports/
14 | /tsconfig.eslint.json
15 | /tsconfig.jest.json
16 | /tsconfig.json
17 | cdk.out/
18 | dist
19 | junit.xml
20 | tsconfig.tsbuildinfo
21 | !/lib
22 | !/lib/**/*.d.ts
23 | !/lib/**/*.js
24 |
--------------------------------------------------------------------------------
/source/constructs/bin/constructs.ts:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as cdk from '@aws-cdk/core';
5 | import { BootstraplessStackSynthesizer } from 'cdk-bootstrapless-synthesizer';
6 | import { ConstructsStack, ECSImageHandlerStack, LambdaImageHandlerStack } from '../lib/constructs-stack';
7 |
8 | const app = new cdk.App();
9 | new ConstructsStack(app, 'ConstructsStack');
10 | const ecsStack = new ECSImageHandlerStack(app, 'serverless-ecs-image-handler-stack', {
11 | stackName: process.env.STACK_NAME,
12 | env: {
13 | account: process.env.CDK_DEFAULT_ACCOUNT,
14 | region: process.env.CDK_DEPLOY_REGION,
15 | },
16 | });
17 |
18 | cdk.Tags.of(ecsStack).add('name', 'serverless-ecs-image-handler');
19 | const stackTags = ecsStack.node.tryGetContext('stack_tags');
20 | if (!!stackTags) {
21 | for (const [key, value] of Object.entries(stackTags)) {
22 | cdk.Tags.of(ecsStack).add(key, value as string);
23 | }
24 | }
25 |
26 | new LambdaImageHandlerStack(app, 'lambda-image-handler-cn', {
27 | isChinaRegion: true,
28 | stackName: process.env.STACK_NAME,
29 | synthesizer: synthesizer(),
30 | });
31 |
32 | new LambdaImageHandlerStack(app, 'lambda-image-handler', {
33 | stackName: process.env.STACK_NAME,
34 | synthesizer: synthesizer(),
35 | });
36 |
37 | // new LambdaImageHandlerStack(app, 'lambda-image-handler-stack', {
38 | // stackName: process.env.STACK_NAME,
39 | // synthesizer: synthesizer(),
40 | // env: {
41 | // account: process.env.CDK_DEFAULT_ACCOUNT,
42 | // region: process.env.CDK_DEPLOY_REGION,
43 | // },
44 | // });
45 |
46 | function synthesizer() {
47 | return process.env.USE_BSS ? new BootstraplessStackSynthesizer() : undefined;
48 | }
49 |
--------------------------------------------------------------------------------
/source/constructs/cdk.context.json:
--------------------------------------------------------------------------------
1 | {
2 | "buckets": [
3 | "",
4 | ""
5 | ],
6 | "//use_vpc_id": "vpc-id",
7 | "//subnet_ids": [
8 | "",
9 | ""
10 | ] ,
11 | "//enable_public_alb": true,
12 | "//enable_cloudfront": true,
13 | "secret_arn": "arn:aws:secretsmanager:::secret:-",
14 | "stack_tags":{"key1":"value1", "key2":"value2"},
15 | "config_json_parameter_name": "",
16 | "ecs_desired_count": 10,
17 | "env": {
18 | "SHARP_QUEUE_LIMIT": "1",
19 | "CACHE_TTL_SEC": "300",
20 | "CACHE_MAX_ITEMS": "10000",
21 | "CACHE_MAX_SIZE_MB": "1024"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/source/constructs/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "npx ts-node --prefer-ts-exts bin/constructs.ts",
3 | "context": {
4 | "@aws-cdk/core:enableStackNameDuplicates": "false",
5 | "aws-cdk:enableDiffNoFail": "true"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/source/constructs/images/01-edit-bucket-policy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/constructs/images/01-edit-bucket-policy.png
--------------------------------------------------------------------------------
/source/constructs/images/02-edit-bucket-policy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/constructs/images/02-edit-bucket-policy.png
--------------------------------------------------------------------------------
/source/constructs/lib/api.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "ServerlessImageHandler"
5 | },
6 | "basePath": "/image",
7 | "schemes": [ "https" ],
8 | "paths": {
9 | "/{proxy+}": {
10 | "x-amazon-apigateway-any-method": {
11 | "produces": [ "application/json" ],
12 | "parameters": [
13 | {
14 | "name": "proxy",
15 | "in": "path",
16 | "required": true,
17 | "type": "string"
18 | },
19 | {
20 | "name": "signature",
21 | "in": "query",
22 | "description": "Signature of the image",
23 | "required": false,
24 | "type": "string"
25 | }
26 | ],
27 | "responses": {},
28 | "x-amazon-apigateway-integration": {
29 | "responses": {
30 | "default": { "statusCode": "200" }
31 | },
32 | "uri": {
33 | "Fn::Join": [
34 | "",
35 | [
36 | "arn:aws:apigateway:",
37 | {
38 | "Ref": "AWS::Region"
39 | },
40 | ":",
41 | "lambda:path/2015-03-31/functions/",
42 | {
43 | "Fn::GetAtt": [
44 | "ImageHandlerFunction",
45 | "Arn"
46 | ]
47 | },
48 | "/invocations"
49 | ]
50 | ]
51 | },
52 | "passthroughBehavior": "when_no_match",
53 | "httpMethod": "POST",
54 | "cacheNamespace": "xh7gp9",
55 | "cacheKeyParameters": [ "method.request.path.proxy" ],
56 | "contentHandling": "CONVERT_TO_TEXT",
57 | "type": "aws_proxy"
58 | }
59 | }
60 | }
61 | },
62 | "x-amazon-apigateway-binary-media-types": [
63 | "*/*"
64 | ]
65 | }
--------------------------------------------------------------------------------
/source/constructs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "constructs",
3 | "description": "Serverless Image Handler Constructs",
4 | "version": "5.2.0",
5 | "license": "Apache-2.0",
6 | "bin": {
7 | "constructs": "bin/constructs.js"
8 | },
9 | "scripts": {
10 | "cdk": "cdk",
11 | "compile": "echo nothing to do",
12 | "test:compile": "tsc --noEmit --project tsconfig.jest.json",
13 | "test": "export BUCKET_NAME=TEST SOLUTION_NAME=serverless-image-handler VERSION=TEST_VERSION && rm -fr dist/ && yarn test:compile && jest --passWithNoTests --all --updateSnapshot && yarn eslint",
14 | "test:new-image-handler": "cd ../new-image-handler && yarn test",
15 | "build": "yarn test && yarn compile && yarn synth",
16 | "build:new-image-handler:docker": "cd ../new-image-handler && yarn build:docker",
17 | "test:watch": "export BUCKET_NAME=TEST SOLUTION_NAME=serverless-image-handler VERSION=TEST_VERSION && jest --watch",
18 | "test:update": "export BUCKET_NAME=TEST SOLUTION_NAME=serverless-image-handler VERSION=TEST_VERSION && jest --updateSnapshot",
19 | "upgrade-dependencies": "export CI=0 && npm-check-updates --upgrade --target=minor --reject='projen' && yarn install --check-files && yarn upgrade",
20 | "eslint": "eslint --ext .ts,.tsx --no-error-on-unmatched-pattern bin lib test",
21 | "synth": "cdk synth",
22 | "deploy": "cdk deploy",
23 | "destroy": "cdk destroy",
24 | "diff": "cdk diff",
25 | "postinstall": "cd ../new-image-handler && yarn"
26 | },
27 | "devDependencies": {
28 | "@aws-cdk/assert": "1.124.0",
29 | "@types/jest": "^26.0.24",
30 | "@types/node": "^14.11.2",
31 | "@typescript-eslint/eslint-plugin": "^4.28.1",
32 | "@typescript-eslint/parser": "^4.28.1",
33 | "aws-cdk": "1.124.0",
34 | "eslint": "^7.30.0",
35 | "eslint-import-resolver-node": "^0.3.4",
36 | "eslint-import-resolver-typescript": "^2.4.0",
37 | "eslint-plugin-import": "^2.23.4",
38 | "jest": "^27.0.6",
39 | "jest-junit": "^12",
40 | "json-schema": "^0.3.0",
41 | "npm-check-updates": "^11",
42 | "ts-jest": "^27.0.3",
43 | "ts-node": "^10.0.0",
44 | "typescript": "^4.3.5"
45 | },
46 | "dependencies": {
47 | "@aws-cdk/aws-apigateway": "1.124.0",
48 | "@aws-cdk/aws-apigatewayv2": "1.124.0",
49 | "@aws-cdk/aws-apigatewayv2-integrations": "1.124.0",
50 | "@aws-cdk/aws-cloudfront": "1.124.0",
51 | "@aws-cdk/aws-cloudfront-origins": "1.124.0",
52 | "@aws-cdk/aws-dynamodb": "1.124.0",
53 | "@aws-cdk/aws-ec2": "1.124.0",
54 | "@aws-cdk/aws-ecs": "1.124.0",
55 | "@aws-cdk/aws-ecs-patterns": "1.124.0",
56 | "@aws-cdk/aws-iam": "1.124.0",
57 | "@aws-cdk/aws-lambda": "1.124.0",
58 | "@aws-cdk/aws-logs": "1.124.0",
59 | "@aws-cdk/aws-s3": "1.124.0",
60 | "@aws-cdk/aws-ssm": "1.124.0",
61 | "@aws-cdk/aws-secretsmanager": "1.124.0",
62 | "@aws-cdk/core": "1.124.0",
63 | "@aws-solutions-constructs/aws-apigateway-lambda": "1.124.0",
64 | "@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": "1.124.0",
65 | "@aws-solutions-constructs/aws-cloudfront-s3": "1.124.0",
66 | "@aws-solutions-constructs/core": "1.124.0",
67 | "cdk-bootstrapless-synthesizer": "0.7.15"
68 | },
69 | "engines": {
70 | "node": ">= 12.0.0"
71 | },
72 | "jest": {
73 | "testPathIgnorePatterns": [
74 | "/node_modules/",
75 | "/cdk.out/"
76 | ],
77 | "watchPathIgnorePatterns": [
78 | "/node_modules/",
79 | "/cdk.out/"
80 | ],
81 | "testMatch": [
82 | "**/__tests__/**/*.ts?(x)",
83 | "**/?(*.)+(spec|test).ts?(x)"
84 | ],
85 | "clearMocks": true,
86 | "collectCoverage": true,
87 | "coverageReporters": [
88 | "json",
89 | "lcov",
90 | "clover",
91 | "text"
92 | ],
93 | "coverageDirectory": "coverage",
94 | "coveragePathIgnorePatterns": [
95 | "/node_modules/",
96 | "/cdk.out/"
97 | ],
98 | "reporters": [
99 | "default",
100 | [
101 | "jest-junit",
102 | {
103 | "outputDirectory": "test-reports"
104 | }
105 | ]
106 | ],
107 | "preset": "ts-jest",
108 | "globals": {
109 | "ts-jest": {
110 | "tsconfig": "tsconfig.jest.json"
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/source/constructs/test/constructs.test.ts:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import * as cdk from '@aws-cdk/core';
5 | import * as Constructs from '../lib/constructs-stack';
6 |
7 |
8 | test('Serverless Image Handler Stack', () => {
9 | const app = new cdk.App();
10 | // WHEN
11 | const stack = new Constructs.ConstructsStack(app, 'MyTestStack');
12 | // THEN
13 |
14 | expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
15 | });
16 |
--------------------------------------------------------------------------------
/source/constructs/test/ecs-image-handler.test.ts:
--------------------------------------------------------------------------------
1 | import '@aws-cdk/assert/jest';
2 | import { App } from '@aws-cdk/core';
3 | import { ECSImageHandlerStack } from '../lib/constructs-stack';
4 |
5 | test('Snapshot', () => {
6 | const app = new App({
7 | context: {
8 | buckets: ['bucket-0'],
9 | secret_arn: 'arn:aws:secretsmanager:us-east-9:123456789012:secret:test-aaabbb',
10 | config_json_parameter_name: 'config_json_parameter_name',
11 | },
12 | });
13 | const stack = new ECSImageHandlerStack(app, 'test');
14 |
15 | expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot();
16 | });
--------------------------------------------------------------------------------
/source/constructs/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "alwaysStrict": true,
4 | "declaration": true,
5 | "experimentalDecorators": true,
6 | "inlineSourceMap": true,
7 | "inlineSources": true,
8 | "lib": [
9 | "es2018"
10 | ],
11 | "module": "CommonJS",
12 | "noEmitOnError": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": true,
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "resolveJsonModule": true,
20 | "strict": true,
21 | "strictNullChecks": true,
22 | "strictPropertyInitialization": true,
23 | "stripInternal": true,
24 | "target": "ES2018"
25 | },
26 | "include": [
27 | "bin/**/*.ts",
28 | "lib/**/*.ts",
29 | "test/**/*.ts"
30 | ],
31 | "exclude": [
32 | "node_modules",
33 | "cdk.out",
34 | ]
35 | }
--------------------------------------------------------------------------------
/source/constructs/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "alwaysStrict": true,
4 | "declaration": true,
5 | "experimentalDecorators": true,
6 | "inlineSourceMap": true,
7 | "inlineSources": true,
8 | "lib": [
9 | "es2018"
10 | ],
11 | "module": "CommonJS",
12 | "noEmitOnError": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": true,
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "resolveJsonModule": true,
20 | "strict": true,
21 | "strictNullChecks": true,
22 | "strictPropertyInitialization": true,
23 | "stripInternal": true,
24 | "target": "ES2018"
25 | },
26 | "include": [
27 | "lib/**/*.ts",
28 | "test/**/*.ts"
29 | ],
30 | "exclude": [
31 | "node_modules",
32 | "cdk.out",
33 | ]
34 | }
--------------------------------------------------------------------------------
/source/constructs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist",
4 | "alwaysStrict": true,
5 | "declaration": true,
6 | "experimentalDecorators": true,
7 | "inlineSourceMap": true,
8 | "inlineSources": true,
9 | "lib": [
10 | "es2018"
11 | ],
12 | "module": "CommonJS",
13 | "noEmitOnError": false,
14 | "noFallthroughCasesInSwitch": true,
15 | "noImplicitAny": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "resolveJsonModule": true,
21 | "strict": true,
22 | "strictNullChecks": true,
23 | "strictPropertyInitialization": true,
24 | "stripInternal": true,
25 | "target": "ES2018"
26 | },
27 | "include": [
28 | "bin/**/*.ts",
29 | "lib/**/*.ts",
30 | "test/**/*.ts",
31 | ],
32 | "exclude": [
33 | "node_modules",
34 | "cdk.out"
35 | ]
36 | }
--------------------------------------------------------------------------------
/source/custom-resource/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "custom-resource",
3 | "description": "Serverless Image Handler custom resource",
4 | "main": "index.js",
5 | "author": {
6 | "name": "aws-solutions-builder"
7 | },
8 | "version": "5.2.0",
9 | "private": true,
10 | "dependencies": {
11 | "axios": "^0.21.1",
12 | "uuid": "^8.3.0"
13 | },
14 | "devDependencies": {
15 | "aws-sdk": "2.771.0",
16 | "axios-mock-adapter": "^1.18.2",
17 | "jest": "^26.4.2"
18 | },
19 | "scripts": {
20 | "pretest": "npm run build:init && npm install",
21 | "test": "jest test/*.spec.js --coverage --silent",
22 | "build:init": "rm -rf dist && rm -rf node_modules",
23 | "build:zip": "zip -rq custom-resource.zip .",
24 | "build:dist": "mkdir dist && mv custom-resource.zip dist/",
25 | "build": "npm run build:init && npm install --production && npm run build:zip && npm run build:dist"
26 | },
27 | "license": "Apache-2.0"
28 | }
29 |
--------------------------------------------------------------------------------
/source/demo-ui/scripts.js:
--------------------------------------------------------------------------------
1 | /*********************************************************************************************************************
2 | * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance *
5 | * with the License. A copy of the License is located at *
6 | * *
7 | * http://www.apache.org/licenses/LICENSE-2.0 *
8 | * *
9 | * or in the 'license' file accompanying this file. This file is distributed on an 'AS IS' BASIS, WITHOUT WARRANTIES *
10 | * OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions *
11 | * and limitations under the License. *
12 | *********************************************************************************************************************/
13 |
14 | function importOriginalImage() {
15 | // Gather the bucket name and image key
16 | const bucketName = $(`#txt-bucket-name`).first().val();
17 | const keyName = $(`#txt-key-name`).first().val();
18 | // Assemble the image request
19 | const request = {
20 | bucket: bucketName,
21 | key: keyName
22 | }
23 | const strRequest = JSON.stringify(request);
24 | const encRequest = btoa(strRequest);
25 | // Import the image data into the element
26 | $(`#img-original`)
27 | .attr(`src`, `${appVariables.apiEndpoint}/${encRequest}`)
28 | .attr(`data-bucket`, bucketName)
29 | .attr(`data-key`, keyName);
30 | }
31 |
32 | function getPreviewImage() {
33 | // Gather the editor inputs
34 | const _width = $(`#editor-width`).first().val();
35 | const _height = $(`#editor-height`).first().val();
36 | const _resize = $(`#editor-resize-mode`).first().val();
37 | const _fillColor = $(`#editor-fill-color`).first().val();
38 | const _backgroundColor = $(`#editor-background-color`).first().val();
39 | const _grayscale = $(`#editor-grayscale`).first().prop("checked");
40 | const _flip = $(`#editor-flip`).first().prop("checked");
41 | const _flop = $(`#editor-flop`).first().prop("checked");
42 | const _negative = $(`#editor-negative`).first().prop("checked");
43 | const _flatten = $(`#editor-flatten`).first().prop("checked");
44 | const _normalize = $(`#editor-normalize`).first().prop("checked");
45 | const _rgb = $(`#editor-rgb`).first().val();
46 | const _smartCrop = $(`#editor-smart-crop`).first().prop("checked");
47 | const _smartCropIndex = $(`#editor-smart-crop-index`).first().val();
48 | const _smartCropPadding = $(`#editor-smart-crop-padding`).first().val();
49 | // Setup the edits object
50 | const _edits = {}
51 | _edits.resize = {};
52 | if (_resize !== "Disabled") {
53 | if (_width !== "") { _edits.resize.width = Number(_width) }
54 | if (_height !== "") { _edits.resize.height = Number(_height) }
55 | _edits.resize.fit = _resize;
56 | }
57 | if (_fillColor !== "") { _edits.resize.background = hexToRgbA(_fillColor, 1) }
58 | if (_backgroundColor !== "") { _edits.flatten = { background: hexToRgbA(_backgroundColor, undefined) }}
59 | if (_grayscale) { _edits.grayscale = _grayscale }
60 | if (_flip) { _edits.flip = _flip }
61 | if (_flop) { _edits.flop = _flop }
62 | if (_negative) { _edits.negate = _negative }
63 | if (_flatten) { _edits.flatten = _flatten }
64 | if (_normalize) { _edits.normalise = _normalize }
65 | if (_rgb !== "") {
66 | const input = _rgb.replace(/\s+/g, '');
67 | const arr = input.split(',');
68 | const rgb = { r: Number(arr[0]), g: Number(arr[1]), b: Number(arr[2]) };
69 | _edits.tint = rgb
70 | }
71 | if (_smartCrop) {
72 | _edits.smartCrop = {};
73 | if (_smartCropIndex !== "") { _edits.smartCrop.faceIndex = Number(_smartCropIndex) }
74 | if (_smartCropPadding !== "") { _edits.smartCrop.padding = Number(_smartCropPadding) }
75 | }
76 | if (Object.keys(_edits.resize).length === 0) { delete _edits.resize };
77 | // Gather the bucket and key names
78 | const bucketName = $(`#img-original`).first().attr(`data-bucket`);
79 | const keyName = $(`#img-original`).first().attr(`data-key`);
80 | // Set up the request body
81 | const request = {
82 | bucket: bucketName,
83 | key: keyName,
84 | edits: _edits
85 | }
86 | if (Object.keys(request.edits).length === 0) { delete request.edits };
87 | console.log(request);
88 | // Setup encoded request
89 | const str = JSON.stringify(request);
90 | const enc = btoa(str);
91 | // Fill the preview image
92 | $(`#img-preview`).attr(`src`, `${appVariables.apiEndpoint}/${enc}`);
93 | // Fill the request body field
94 | $(`#preview-request-body`).html(JSON.stringify(request, undefined, 2));
95 | // Fill the encoded URL field
96 | $(`#preview-encoded-url`).val(`${appVariables.apiEndpoint}/${enc}`);
97 | }
98 |
99 | function hexToRgbA(hex, _alpha) {
100 | var c;
101 | if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
102 | c= hex.substring(1).split('');
103 | if(c.length== 3){
104 | c= [c[0], c[0], c[1], c[1], c[2], c[2]];
105 | }
106 | c= '0x'+c.join('');
107 | return { r: ((c>>16)&255), g: ((c>>8)&255), b: (c&255), alpha: Number(_alpha)};
108 | }
109 | throw new Error('Bad Hex');
110 | }
111 |
112 | function resetEdits() {
113 | $('.form-control').val('');
114 | document.getElementById('editor-resize-mode').selectedIndex = 0;
115 | $(".form-check-input").prop('checked', false);
116 | }
--------------------------------------------------------------------------------
/source/demo-ui/style.css:
--------------------------------------------------------------------------------
1 | .header {
2 | background: #37474f !important;
3 | color: #fff !important;
4 | padding: 10px 10px 10px 20px !important;
5 | font-size: 1.2em !important;
6 | }
7 | .header-italics {
8 | color: #b0bec5 !important;
9 | }
10 | .content {
11 | margin-top: 25px !important;
12 | }
13 | .card-original-image {
14 | margin-top: 20px !important;
15 | }
16 | #img-original {
17 | max-height: 100% !important;
18 | max-width: 100% !important;
19 | }
20 | #img-preview {
21 | max-height: 100% !important;
22 | max-width: 100% !important;
23 | }
24 |
25 |
26 |
27 | .gallery-item {
28 | width: 30%;
29 | margin: 1%;
30 | max-height: auto;
31 | border: 1px solid gray;
32 | border-radius: 4px;
33 | cursor: pointer;
34 | }
35 | .gallery-item-selected {
36 | border: 4px solid #ffa726;
37 | }
38 | .preview-code-block {
39 | background: #cfd8dc !important;
40 | padding: 8px;
41 | border-radius: 4px;
42 | }
43 | .preview-code-block code {
44 | color: black !important;
45 | }
--------------------------------------------------------------------------------
/source/image-handler/index.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | const AWS = require('aws-sdk');
5 | const s3 = new AWS.S3();
6 | const rekognition = new AWS.Rekognition();
7 | const secretsManager = new AWS.SecretsManager();
8 |
9 | const ImageRequest = require('./image-request.js');
10 | const ImageHandler = require('./image-handler.js');
11 |
12 | exports.handler = async (event) => {
13 | console.log(event);
14 | const imageRequest = new ImageRequest(s3, secretsManager);
15 | const imageHandler = new ImageHandler(s3, rekognition);
16 | const isAlb = event.requestContext && event.requestContext.hasOwnProperty('elb');
17 |
18 | try {
19 | const request = await imageRequest.setup(event);
20 | console.log(request);
21 |
22 | const processedRequest = await imageHandler.process(request);
23 | const headers = getResponseHeaders(false, isAlb);
24 | headers["Content-Type"] = request.ContentType;
25 | headers["Expires"] = request.Expires;
26 | headers["Last-Modified"] = request.LastModified;
27 | headers["Cache-Control"] = request.CacheControl;
28 |
29 | if (request.headers) {
30 | // Apply the custom headers overwritting any that may need overwriting
31 | for (let key in request.headers) {
32 | headers[key] = request.headers[key];
33 | }
34 | }
35 |
36 | return {
37 | statusCode: 200,
38 | isBase64Encoded: true,
39 | headers : headers,
40 | body: processedRequest
41 | };
42 | } catch (err) {
43 | console.error(err);
44 |
45 | // Default fallback image
46 | if (process.env.ENABLE_DEFAULT_FALLBACK_IMAGE === 'Yes'
47 | && process.env.DEFAULT_FALLBACK_IMAGE_BUCKET
48 | && process.env.DEFAULT_FALLBACK_IMAGE_BUCKET.replace(/\s/, '') !== ''
49 | && process.env.DEFAULT_FALLBACK_IMAGE_KEY
50 | && process.env.DEFAULT_FALLBACK_IMAGE_KEY.replace(/\s/, '') !== '') {
51 | try {
52 | const bucket = process.env.DEFAULT_FALLBACK_IMAGE_BUCKET;
53 | const objectKey = process.env.DEFAULT_FALLBACK_IMAGE_KEY;
54 | const defaultFallbackImage = await s3.getObject({ Bucket: bucket, Key: objectKey }).promise();
55 | const headers = getResponseHeaders(false, isAlb);
56 | headers['Content-Type'] = defaultFallbackImage.ContentType;
57 | headers['Last-Modified'] = defaultFallbackImage.LastModified;
58 | headers['Cache-Control'] = 'max-age=31536000,public';
59 |
60 | return {
61 | statusCode: err.status ? err.status : 500,
62 | isBase64Encoded: true,
63 | headers: headers,
64 | body: defaultFallbackImage.Body.toString('base64')
65 | };
66 | } catch (error) {
67 | console.error('Error occurred while getting the default fallback image.', error);
68 | }
69 | }
70 |
71 | if (err.status) {
72 | return {
73 | statusCode: err.status,
74 | isBase64Encoded: false,
75 | headers : getResponseHeaders(true, isAlb),
76 | body: JSON.stringify(err)
77 | };
78 | } else {
79 | return {
80 | statusCode: 500,
81 | isBase64Encoded: false,
82 | headers : getResponseHeaders(true, isAlb),
83 | body: JSON.stringify({ message: 'Internal error. Please contact the system administrator.', code: 'InternalError', status: 500 })
84 | };
85 | }
86 | }
87 | }
88 |
89 | /**
90 | * Generates the appropriate set of response headers based on a success
91 | * or error condition.
92 | * @param {boolean} isErr - has an error been thrown?
93 | * @param {boolean} isAlb - is the request from ALB?
94 | * @return {object} - Headers object
95 | */
96 | const getResponseHeaders = (isErr = false, isAlb = false) => {
97 | const corsEnabled = (process.env.CORS_ENABLED === "Yes");
98 | const headers = {
99 | "Access-Control-Allow-Methods": "GET",
100 | "Access-Control-Allow-Headers": "Content-Type, Authorization"
101 | }
102 | if (!isAlb) {
103 | headers["Access-Control-Allow-Credentials"] = true;
104 | }
105 | if (corsEnabled) {
106 | headers["Access-Control-Allow-Origin"] = process.env.CORS_ORIGIN;
107 | }
108 | if (isErr) {
109 | headers["Content-Type"] = "application/json"
110 | }
111 | return headers;
112 | }
--------------------------------------------------------------------------------
/source/image-handler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-handler",
3 | "description": "A Lambda function for performing on-demand image edits and manipulations.",
4 | "main": "index.js",
5 | "author": {
6 | "name": "aws-solutions-builder"
7 | },
8 | "version": "5.2.0",
9 | "private": true,
10 | "dependencies": {
11 | "color": "3.1.3",
12 | "color-name": "1.1.4",
13 | "sharp": "^0.27.0"
14 | },
15 | "devDependencies": {
16 | "aws-sdk": "2.771.0",
17 | "jest": "^26.4.2"
18 | },
19 | "scripts": {
20 | "pretest": "npm run build:init && npm install",
21 | "test": "jest test/*.spec.js --coverage --silent",
22 | "build:init": "rm -rf package-lock.json dist/ node_modules/",
23 | "build:zip": "zip -rq image-handler.zip .",
24 | "build:dist": "mkdir dist && mv image-handler.zip dist/",
25 | "build": "npm run build:init && npm install --arch=x64 --platform=linux --production && npm run build:zip && npm run build:dist"
26 | },
27 | "license": "Apache-2.0"
28 | }
29 |
--------------------------------------------------------------------------------
/source/image-handler/test/image/test.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/image-handler/test/image/test.jpg
--------------------------------------------------------------------------------
/source/new-image-handler/.dockerignore:
--------------------------------------------------------------------------------
1 | Dockerfile
2 | Dockerfile.lambda.deps
3 | Dockerfile.lambda
4 | Dockerfile*
5 | node_modules
6 | coverage
7 | test-reports
8 | **/*.md
9 | **/*.bin
--------------------------------------------------------------------------------
/source/new-image-handler/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true,
4 | "node": true
5 | },
6 | "root": true,
7 | "plugins": [
8 | "@typescript-eslint",
9 | "import"
10 | ],
11 | "parser": "@typescript-eslint/parser",
12 | "parserOptions": {
13 | "ecmaVersion": 2018,
14 | "sourceType": "module",
15 | "project": "./tsconfig.eslint.json"
16 | },
17 | "extends": [
18 | "plugin:import/typescript"
19 | ],
20 | "settings": {
21 | "import/parsers": {
22 | "@typescript-eslint/parser": [
23 | ".ts",
24 | ".tsx"
25 | ]
26 | },
27 | "import/resolver": {
28 | "node": {},
29 | "typescript": {
30 | "project": "./tsconfig.eslint.json"
31 | }
32 | }
33 | },
34 | "ignorePatterns": [
35 | "*.js",
36 | "!.projenrc.js",
37 | "*.d.ts",
38 | "node_modules/",
39 | "*.generated.ts",
40 | "coverage"
41 | ],
42 | "rules": {
43 | "radix": "error",
44 | "space-infix-ops": "error",
45 | "spaced-comment": [
46 | "error",
47 | "always"
48 | ],
49 | "no-var": "error",
50 | "eqeqeq": "error",
51 | "space-in-parens": [
52 | "error",
53 | "never"
54 | ],
55 | "indent": [
56 | "off"
57 | ],
58 | "@typescript-eslint/indent": [
59 | "error",
60 | 2
61 | ],
62 | "quotes": [
63 | "error",
64 | "single",
65 | {
66 | "avoidEscape": true
67 | }
68 | ],
69 | "comma-dangle": [
70 | "error",
71 | "always-multiline"
72 | ],
73 | "comma-spacing": [
74 | "error",
75 | {
76 | "before": false,
77 | "after": true
78 | }
79 | ],
80 | "no-multi-spaces": [
81 | "error",
82 | {
83 | "ignoreEOLComments": false
84 | }
85 | ],
86 | "array-bracket-spacing": [
87 | "error",
88 | "never"
89 | ],
90 | "array-bracket-newline": [
91 | "error",
92 | "consistent"
93 | ],
94 | "object-curly-spacing": [
95 | "error",
96 | "always"
97 | ],
98 | "object-curly-newline": [
99 | "error",
100 | {
101 | "multiline": true,
102 | "consistent": true
103 | }
104 | ],
105 | "object-property-newline": [
106 | "error",
107 | {
108 | "allowAllPropertiesOnSameLine": true
109 | }
110 | ],
111 | "keyword-spacing": [
112 | "error"
113 | ],
114 | "brace-style": [
115 | "error",
116 | "1tbs",
117 | {
118 | "allowSingleLine": true
119 | }
120 | ],
121 | "space-before-blocks": [
122 | "error"
123 | ],
124 | "curly": [
125 | "error",
126 | "multi-line",
127 | "consistent"
128 | ],
129 | "@typescript-eslint/member-delimiter-style": [
130 | "error"
131 | ],
132 | "semi": [
133 | "error",
134 | "always"
135 | ],
136 | "max-len": [
137 | "error",
138 | {
139 | "code": 150,
140 | "ignoreUrls": true,
141 | "ignoreStrings": true,
142 | "ignoreTemplateLiterals": true,
143 | "ignoreComments": true,
144 | "ignoreRegExpLiterals": true
145 | }
146 | ],
147 | "quote-props": [
148 | "error",
149 | "consistent-as-needed"
150 | ],
151 | "@typescript-eslint/no-require-imports": [
152 | "error"
153 | ],
154 | "import/no-extraneous-dependencies": [
155 | "error",
156 | {
157 | "devDependencies": [
158 | "**/test/**",
159 | "**/build-tools/**"
160 | ],
161 | "optionalDependencies": false,
162 | "peerDependencies": true
163 | }
164 | ],
165 | "import/no-unresolved": [
166 | "error"
167 | ],
168 | "import/order": [
169 | "warn",
170 | {
171 | "groups": [
172 | "builtin",
173 | "external"
174 | ],
175 | "alphabetize": {
176 | "order": "asc",
177 | "caseInsensitive": true
178 | }
179 | }
180 | ],
181 | "no-duplicate-imports": [
182 | "error"
183 | ],
184 | "no-shadow": [
185 | "off"
186 | ],
187 | "@typescript-eslint/no-shadow": [
188 | "error"
189 | ],
190 | "key-spacing": [
191 | "error"
192 | ],
193 | "no-multiple-empty-lines": [
194 | "error"
195 | ],
196 | "@typescript-eslint/no-floating-promises": [
197 | "error"
198 | ],
199 | "no-return-await": [
200 | "off"
201 | ],
202 | "@typescript-eslint/return-await": [
203 | "error"
204 | ],
205 | "no-trailing-spaces": [
206 | "error"
207 | ],
208 | "dot-notation": [
209 | "error"
210 | ],
211 | "no-bitwise": [
212 | "error"
213 | ],
214 | "@typescript-eslint/member-ordering": [
215 | "error",
216 | {
217 | "default": [
218 | "public-static-field",
219 | "public-static-method",
220 | "protected-static-field",
221 | "protected-static-method",
222 | "private-static-field",
223 | "private-static-method",
224 | "field",
225 | "constructor",
226 | "method"
227 | ]
228 | }
229 | ]
230 | },
231 | "overrides": [
232 | {
233 | "files": [
234 | ".projenrc.js"
235 | ],
236 | "rules": {
237 | "@typescript-eslint/no-require-imports": "off",
238 | "import/no-extraneous-dependencies": "off"
239 | }
240 | }
241 | ]
242 | }
--------------------------------------------------------------------------------
/source/new-image-handler/.gitattributes:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | /.gitattributes linguist-generated
4 | /.projen/** linguist-generated
--------------------------------------------------------------------------------
/source/new-image-handler/.gitignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | *.lcov
3 | *.log
4 | *.pid
5 | *.pid.lock
6 | *.seed
7 | *.tgz
8 | *.tsbuildinfo
9 | .cache
10 | .eslintcache
11 | .nyc_output
12 | .yarn-integrity
13 | /coverage
14 | /dist
15 | /lib
16 | /test-reports/
17 | build/Release
18 | coverage
19 | jspm_packages/
20 | junit.xml
21 | lerna-debug.log*
22 | lib-cov
23 | logs
24 | node_modules/
25 | npm-debug.log*
26 | pids
27 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
28 | yarn-debug.log*
29 | yarn-error.log*
30 | !/.eslintrc.json
31 | !/.gitattributes
32 | !/.npmignore
33 | !/.projen/deps.json
34 | !/.projen/tasks.json
35 | !/.projenrc.js
36 | !/LICENSE
37 | !/package.json
38 | !/src
39 | !/test
40 | !/tsconfig.eslint.json
41 | !/tsconfig.jest.json
42 | !/tsconfig.json
43 |
--------------------------------------------------------------------------------
/source/new-image-handler/.npmignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | /.eslintrc.json
3 | /.github
4 | /.idea
5 | /.projen
6 | /.projenrc.js
7 | /.vscode
8 | /coverage
9 | /src
10 | /test
11 | /test-reports/
12 | /tsconfig.eslint.json
13 | /tsconfig.jest.json
14 | /tsconfig.json
15 | dist
16 | junit.xml
17 | tsconfig.tsbuildinfo
18 | !/lib
19 | !/lib/**/*.d.ts
20 | !/lib/**/*.js
21 |
--------------------------------------------------------------------------------
/source/new-image-handler/Dockerfile:
--------------------------------------------------------------------------------
1 | # FROM public.ecr.aws/docker/library/node:14-alpine3.16 as node-with-vips
2 |
3 | # ARG VIPS_VERSION=8.11.3
4 |
5 | # RUN set -x -o pipefail \
6 | # && wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
7 | # && apk update \
8 | # && apk upgrade \
9 | # && apk add build-base \
10 | # zlib-dev libxml2-dev glib-dev gobject-introspection-dev \
11 | # libjpeg-turbo-dev libexif-dev lcms2-dev fftw-dev giflib-dev libpng-dev \
12 | # libwebp-dev orc-dev tiff-dev poppler-dev librsvg-dev libgsf-dev openexr-dev \
13 | # libheif-dev libimagequant-dev pango-dev cfitsio-dev expat-dev openjpeg-dev imagemagick-dev \
14 | # py-gobject3-dev \
15 | # && cd /tmp/vips-${VIPS_VERSION} \
16 | # && ./configure --prefix=/usr \
17 | # --with-magick \
18 | # --disable-static \
19 | # --disable-dependency-tracking \
20 | # --enable-silent-rules \
21 | # && make -s install-strip \
22 | # && cd $OLDPWD \
23 | # && rm -rf /tmp/vips-${VIPS_VERSION} \
24 | # # && apk del --purge vips-dependencies \
25 | # && rm -rf /var/cache/apk/*
26 |
27 | FROM public.ecr.aws/docker/library/node:14-alpine3.16 as builder
28 |
29 | WORKDIR /app
30 |
31 | COPY package.json yarn.lock /app/
32 |
33 | # FIXME: try to remove font-noto when https://github.com/lovell/sharp/issues/3393 is fixed.
34 | RUN apk update && \
35 | apk add fontconfig font-noto
36 |
37 | RUN yarn
38 |
39 | COPY . .
40 |
41 | RUN yarn build
42 |
43 |
44 | FROM public.ecr.aws/docker/library/node:14-alpine3.16
45 |
46 | WORKDIR /app
47 |
48 | COPY package.json yarn.lock /app/
49 | COPY ./fonts/* /usr/share/fonts/
50 |
51 | # FIXME: try to remove font-noto when https://github.com/lovell/sharp/issues/3393 is fixed.
52 | RUN apk update && \
53 | apk add fontconfig font-noto ffmpeg
54 |
55 | ENV NODE_ENV=production
56 |
57 | RUN yarn --production && \
58 | yarn cache clean --all
59 |
60 | COPY --from=builder /app/lib /app/lib
61 | # COPY test/fixtures /app/lib/test/fixtures
62 |
63 | EXPOSE 8080
64 |
65 | CMD ["node", "/app/lib/src/index.js"]
--------------------------------------------------------------------------------
/source/new-image-handler/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | # FROM public.ecr.aws/docker/library/node:14-slim as node-with-vips
2 |
3 | # ARG VIPS_VERSION=8.11.3
4 |
5 | # RUN set -x -o pipefail \
6 | # && wget -O- https://github.com/libvips/libvips/releases/download/v${VIPS_VERSION}/vips-${VIPS_VERSION}.tar.gz | tar xzC /tmp \
7 | # && apk update \
8 | # && apk upgrade \
9 | # && apk add build-base \
10 | # zlib-dev libxml2-dev glib-dev gobject-introspection-dev \
11 | # libjpeg-turbo-dev libexif-dev lcms2-dev fftw-dev giflib-dev libpng-dev \
12 | # libwebp-dev orc-dev tiff-dev poppler-dev librsvg-dev libgsf-dev openexr-dev \
13 | # libheif-dev libimagequant-dev pango-dev cfitsio-dev expat-dev openjpeg-dev imagemagick-dev \
14 | # py-gobject3-dev \
15 | # && cd /tmp/vips-${VIPS_VERSION} \
16 | # && ./configure --prefix=/usr \
17 | # --with-magick \
18 | # --disable-static \
19 | # --disable-dependency-tracking \
20 | # --enable-silent-rules \
21 | # && make -s install-strip \
22 | # && cd $OLDPWD \
23 | # && rm -rf /tmp/vips-${VIPS_VERSION} \
24 | # # && apk del --purge vips-dependencies \
25 | # && rm -rf /var/cache/apk/*
26 |
27 |
28 | # FROM node-with-vips as builder
29 |
30 | # WORKDIR /app
31 |
32 | # COPY package.json yarn.lock /app/
33 |
34 | # RUN apk update && \
35 | # apk add fontconfig \
36 | # imagemagick=~7.0
37 |
38 | # RUN yarn
39 |
40 | # COPY . .
41 |
42 | # RUN yarn build
43 |
44 |
45 | FROM public.ecr.aws/docker/library/node:14-alpine3.16
46 |
47 | WORKDIR /app
48 |
49 | COPY package.json yarn.lock /app/
50 | COPY ./fonts/* /usr/share/fonts/
51 |
52 | # FIXME: try to remove font-noto when https://github.com/lovell/sharp/issues/3393 is fixed.
53 | RUN apk update && \
54 | apk add fontconfig font-noto ffmpeg
55 |
56 | # ENV NODE_ENV=production
57 |
58 | RUN yarn && \
59 | yarn cache clean --all
60 |
61 | COPY . .
62 |
63 | EXPOSE 8080
64 |
65 | CMD yarn watch-server
--------------------------------------------------------------------------------
/source/new-image-handler/Dockerfile.lambda:
--------------------------------------------------------------------------------
1 | FROM public.ecr.aws/sam/build-nodejs14.x
2 |
3 | WORKDIR /app
4 |
5 | COPY package.json yarn.lock /app/
6 |
7 | RUN npx yarn
8 |
9 | COPY . /app/
10 |
11 | RUN npx yarn build && \
12 | mkdir -p /asset/ && \
13 | cp -au lib/src /asset/
--------------------------------------------------------------------------------
/source/new-image-handler/Dockerfile.lambda.deps:
--------------------------------------------------------------------------------
1 | # FROM public.ecr.aws/sam/build-nodejs14.x as ImageMagickBuilder
2 |
3 | # WORKDIR /ws
4 |
5 | # RUN yum install -y yum-plugin-ovl && \
6 | # yum update -y && \
7 | # yum install -y cmake
8 |
9 | # COPY ImageMagick.Makefile /ws
10 |
11 | # RUN make -f ImageMagick.Makefile libs TARGET_DIR=/opt
12 |
13 | # RUN make -f ImageMagick.Makefile all TARGET_DIR=/opt
14 |
15 |
16 | FROM public.ecr.aws/sam/build-nodejs14.x
17 |
18 | WORKDIR /app
19 |
20 | COPY package.json yarn.lock /app/
21 |
22 | RUN npx yarn install --prod && \
23 | mkdir -p /asset/nodejs && \
24 | cp -au node_modules /asset/nodejs/
25 |
26 | # COPY --from=ImageMagickBuilder /opt/bin/magick /opt/bin/identify /opt/bin/convert /asset/bin/
27 |
28 | # RUN strip /asset/bin/*
29 |
30 | RUN du -sh /asset/
--------------------------------------------------------------------------------
/source/new-image-handler/ImageMagick.Makefile:
--------------------------------------------------------------------------------
1 | LIBPNG_VERSION ?= 1.6.37
2 | LIBJPG_VERSION ?= 9c
3 | OPENJP2_VERSION ?= 2.3.1
4 | LIBTIFF_VERSION ?= 4.0.9
5 | BZIP2_VERSION ?= 1.0.6
6 | LIBWEBP_VERSION ?= 0.6.1
7 | IMAGEMAGICK_VERSION ?= 7.0.8-45
8 |
9 | TARGET_DIR ?= /opt/
10 | PROJECT_ROOT = $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
11 | CACHE_DIR=$(PROJECT_ROOT)build/cache
12 |
13 | .ONESHELL:
14 |
15 | CONFIGURE = PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
16 | ./configure \
17 | CPPFLAGS=-I$(CACHE_DIR)/include \
18 | LDFLAGS=-L$(CACHE_DIR)/lib \
19 | --disable-dependency-tracking \
20 | --disable-shared \
21 | --enable-static \
22 | --prefix=$(CACHE_DIR)
23 |
24 | ## libjpg
25 |
26 | LIBJPG_SOURCE=jpegsrc.v$(LIBJPG_VERSION).tar.gz
27 |
28 | $(LIBJPG_SOURCE):
29 | curl -LO http://ijg.org/files/$(LIBJPG_SOURCE)
30 |
31 | $(CACHE_DIR)/lib/libjpeg.a: $(LIBJPG_SOURCE)
32 | tar xf $<
33 | cd jpeg*
34 | $(CONFIGURE)
35 | make
36 | make install
37 |
38 |
39 | ## libpng
40 |
41 | LIBPNG_SOURCE=libpng-$(LIBPNG_VERSION).tar.xz
42 |
43 | $(LIBPNG_SOURCE):
44 | curl -LO http://prdownloads.sourceforge.net/libpng/$(LIBPNG_SOURCE)
45 |
46 | $(CACHE_DIR)/lib/libpng.a: $(LIBPNG_SOURCE)
47 | tar xf $<
48 | cd libpng*
49 | $(CONFIGURE)
50 | make
51 | make install
52 |
53 | # libbz2
54 |
55 | BZIP2_SOURCE=bzip2-$(BZIP2_VERSION).tar.gz
56 |
57 | $(BZIP2_SOURCE):
58 | curl -LO http://prdownloads.sourceforge.net/bzip2/bzip2-$(BZIP2_VERSION).tar.gz
59 |
60 | $(CACHE_DIR)/lib/libbz2.a: $(BZIP2_SOURCE)
61 | tar xf $<
62 | cd bzip2-*
63 | make libbz2.a
64 | make install PREFIX=$(CACHE_DIR)
65 |
66 | # libtiff
67 |
68 | LIBTIFF_SOURCE=tiff-$(LIBTIFF_VERSION).tar.gz
69 |
70 | $(LIBTIFF_SOURCE):
71 | curl -LO http://download.osgeo.org/libtiff/$(LIBTIFF_SOURCE)
72 |
73 | $(CACHE_DIR)/lib/libtiff.a: $(LIBTIFF_SOURCE) $(CACHE_DIR)/lib/libjpeg.a
74 | tar xf $<
75 | cd tiff-*
76 | $(CONFIGURE)
77 | make
78 | make install
79 |
80 | # libwebp
81 |
82 | LIBWEBP_SOURCE=libwebp-$(LIBWEBP_VERSION).tar.gz
83 |
84 | $(LIBWEBP_SOURCE):
85 | curl -L https://github.com/webmproject/libwebp/archive/v$(LIBWEBP_VERSION).tar.gz -o $(LIBWEBP_SOURCE)
86 |
87 | $(CACHE_DIR)/lib/libwebp.a: $(LIBWEBP_SOURCE)
88 | tar xf $<
89 | cd libwebp-*
90 | sh autogen.sh
91 | $(CONFIGURE)
92 | make
93 | make install
94 |
95 | ## libopenjp2
96 |
97 | OPENJP2_SOURCE=openjp2-$(OPENJP2_VERSION).tar.gz
98 |
99 | $(OPENJP2_SOURCE):
100 | curl -L https://github.com/uclouvain/openjpeg/archive/v$(OPENJP2_VERSION).tar.gz -o $(OPENJP2_SOURCE)
101 |
102 |
103 | $(CACHE_DIR)/lib/libopenjp2.a: $(OPENJP2_SOURCE) $(CACHE_DIR)/lib/libpng.a $(CACHE_DIR)/lib/libtiff.a
104 | tar xf $<
105 | cd openjpeg-*
106 | mkdir -p build
107 | cd build
108 | PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig cmake .. \
109 | -DCMAKE_BUILD_TYPE=Release \
110 | -DCMAKE_INSTALL_PREFIX=$(CACHE_DIR) \
111 | -DBUILD_SHARED_LIBS:bool=off \
112 | -DBUILD_CODEC:bool=off
113 | make clean
114 | make install
115 |
116 |
117 | ## ImageMagick
118 |
119 | IMAGE_MAGICK_SOURCE=ImageMagick-$(IMAGEMAGICK_VERSION).tar.gz
120 |
121 | $(IMAGE_MAGICK_SOURCE):
122 | curl -L https://github.com/ImageMagick/ImageMagick/archive/refs/tags/$(IMAGEMAGICK_VERSION).tar.gz -o $(IMAGE_MAGICK_SOURCE)
123 |
124 |
125 | LIBS:=$(CACHE_DIR)/lib/libjpeg.a \
126 | $(CACHE_DIR)/lib/libpng.a \
127 | $(CACHE_DIR)/lib/libopenjp2.a \
128 | $(CACHE_DIR)/lib/libtiff.a \
129 | $(CACHE_DIR)/lib/libbz2.a \
130 | $(CACHE_DIR)/lib/libwebp.a
131 |
132 | $(TARGET_DIR)/bin/identify: $(IMAGE_MAGICK_SOURCE) $(LIBS)
133 | tar xf $<
134 | cd ImageMa*
135 | PKG_CONFIG_PATH=$(CACHE_DIR)/lib/pkgconfig \
136 | ./configure \
137 | CPPFLAGS=-I$(CACHE_DIR)/include \
138 | LDFLAGS=-L$(CACHE_DIR)/lib \
139 | --disable-dependency-tracking \
140 | --disable-shared \
141 | --enable-static \
142 | --prefix=$(TARGET_DIR) \
143 | --enable-delegate-build \
144 | --without-modules \
145 | --disable-docs \
146 | --without-magick-plus-plus \
147 | --without-perl \
148 | --without-x \
149 | --disable-openmp
150 | make clean
151 | make all
152 | make install-strip
153 |
154 | libs: $(LIBS)
155 |
156 | all: $(TARGET_DIR)/bin/identify
--------------------------------------------------------------------------------
/source/new-image-handler/fonts/FangZhengHeiTi-GBK-1.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/fonts/FangZhengHeiTi-GBK-1.ttf
--------------------------------------------------------------------------------
/source/new-image-handler/fonts/README.md:
--------------------------------------------------------------------------------
1 | # Usage Declaretion
2 | Here we use FangZhengHeiTi by default, please take a look the fellow link to make sure your usage is authorized:
3 |
4 | https://www.foundertype.com/index.php/About/powerbus.html
5 |
6 | you can also use other font by changing the font file.
--------------------------------------------------------------------------------
/source/new-image-handler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "new-image-handler",
3 | "scripts": {
4 | "watch-server": "nodemon --ignore test/ --watch src -e ts,tsx --exec ts-node -P tsconfig.json --files src/index.ts",
5 | "serve": "node src/lib/index.js",
6 | "compile": "tsc --project tsconfig.json",
7 | "build": "yarn test && yarn compile",
8 | "build:docker": "docker build -t new-image-handler .",
9 | "build:docker.dev": "docker build -t new-image-handler.dev -f Dockerfile.dev .",
10 | "run:docker.dev": "yarn build:docker.dev && docker run --rm -ti -p 8080:8080 -e NODE_ENV=dev -v $PWD/src:/app/src -v $PWD/test:/app/test new-image-handler.dev",
11 | "run:local": "yarn watch-server",
12 | "test": "rm -fr lib/ && yarn test:compile && jest --passWithNoTests --all --coverageProvider=v8 && yarn eslint",
13 | "test:watch": "jest --watch",
14 | "test:update": "jest --updateSnapshot",
15 | "test:compile": "tsc --noEmit --project tsconfig.jest.json",
16 | "upgrade-dependencies": "export CI=0 && npm-check-updates --upgrade --target=minor --reject='projen' && yarn install --check-files && yarn upgrade",
17 | "watch": "tsc --project tsconfig.json -w",
18 | "benchmark": "ts-node test/bench/perf.ts",
19 | "eslint": "eslint --ext .js,.ts,.tsx --no-error-on-unmatched-pattern src test .projenrc.js"
20 | },
21 | "devDependencies": {
22 | "@types/aws-lambda": "^8.10.95",
23 | "@types/benchmark": "^2.1.1",
24 | "@types/jest": "^26.0.24",
25 | "@types/koa": "^2.13.4",
26 | "@types/koa-bodyparser": "^4.3.7",
27 | "@types/koa-cash": "^4.1.0",
28 | "@types/koa-logger": "^3.1.2",
29 | "@types/koa-router": "^7.4.4",
30 | "@types/node": "^14.18.16",
31 | "@types/sharp": "^0.31.0",
32 | "@typescript-eslint/eslint-plugin": "^4.33.0",
33 | "@typescript-eslint/parser": "^4.33.0",
34 | "benchmark": "^2.1.4",
35 | "eslint": "^7.32.0",
36 | "eslint-import-resolver-node": "^0.3.6",
37 | "eslint-import-resolver-typescript": "^2.7.1",
38 | "eslint-plugin-import": "^2.26.0",
39 | "jest": "^27.5.1",
40 | "jest-junit": "^12",
41 | "jimp": "^0.16.1",
42 | "json-schema": "^0.4.0",
43 | "nodemon": "^2.0.16",
44 | "npm-check-updates": "^11",
45 | "ts-jest": "^27.1.4",
46 | "ts-node": "^10.7.0",
47 | "typescript": "^4.6.4"
48 | },
49 | "dependencies": {
50 | "aws-sdk": "^2.1130.0",
51 | "html-entities": "^2.3.3",
52 | "http-errors": "^1.8.1",
53 | "koa": "^2.13.4",
54 | "koa-bodyparser": "^4.3.0",
55 | "koa-cash": "^4.1.1",
56 | "koa-logger": "^3.2.1",
57 | "koa-router": "^10.1.1",
58 | "lru-cache": "^10.0.0",
59 | "sharp": "^0.31.1"
60 | },
61 | "bundledDependencies": [],
62 | "engines": {
63 | "node": ">= 12.0.0"
64 | },
65 | "license": "Apache-2.0",
66 | "version": "0.0.0",
67 | "jest": {
68 | "testPathIgnorePatterns": [
69 | "/node_modules/",
70 | "/cdk.out/"
71 | ],
72 | "watchPathIgnorePatterns": [
73 | "/node_modules/",
74 | "/cdk.out/"
75 | ],
76 | "testMatch": [
77 | "**/__tests__/**/*.ts?(x)",
78 | "**/?(*.)+(spec|test).ts?(x)"
79 | ],
80 | "clearMocks": true,
81 | "collectCoverage": true,
82 | "coverageReporters": [
83 | "json",
84 | "lcov",
85 | "clover",
86 | "text"
87 | ],
88 | "coverageDirectory": "coverage",
89 | "coveragePathIgnorePatterns": [
90 | "/node_modules/",
91 | "/cdk.out/"
92 | ],
93 | "reporters": [
94 | "default",
95 | [
96 | "jest-junit",
97 | {
98 | "outputDirectory": "test-reports"
99 | }
100 | ]
101 | ],
102 | "preset": "ts-jest",
103 | "globals": {
104 | "ts-jest": {
105 | "tsconfig": "tsconfig.jest.json"
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/source/new-image-handler/src/config.ts:
--------------------------------------------------------------------------------
1 | const {
2 | REGION,
3 | AWS_REGION,
4 | NODE_ENV,
5 | BUCKET,
6 | SRC_BUCKET,
7 | STYLE_TABLE_NAME,
8 | AUTO_WEBP,
9 | SECRET_NAME,
10 | SHARP_QUEUE_LIMIT,
11 | CONFIG_JSON_PARAMETER_NAME,
12 | CACHE_TTL_SEC,
13 | CACHE_MAX_ITEMS,
14 | CACHE_MAX_SIZE_MB,
15 | } = process.env;
16 |
17 | export interface IConfig {
18 | port: number;
19 | region: string;
20 | isProd: boolean;
21 | srcBucket: string;
22 | styleTableName: string;
23 | autoWebp: boolean;
24 | secretName: string;
25 | sharpQueueLimit: number;
26 | configJsonParameterName: string;
27 | CACHE_TTL_SEC: number;
28 | CACHE_MAX_ITEMS: number;
29 | CACHE_MAX_SIZE_MB: number;
30 | }
31 |
32 | function parseInt(s: string) {
33 | return Number.parseInt(s, 10);
34 | }
35 |
36 | const conf: IConfig = {
37 | port: 8080,
38 | region: REGION ?? AWS_REGION ?? 'us-west-2',
39 | isProd: NODE_ENV === 'production',
40 | srcBucket: BUCKET || SRC_BUCKET || 'sih-input',
41 | styleTableName: STYLE_TABLE_NAME || 'style-table-name',
42 | autoWebp: ['yes', '1', 'true'].includes((AUTO_WEBP ?? '').toLowerCase()),
43 | secretName: SECRET_NAME ?? 'X-Client-Authorization',
44 | sharpQueueLimit: parseInt(SHARP_QUEUE_LIMIT ?? '1'),
45 | configJsonParameterName: CONFIG_JSON_PARAMETER_NAME ?? '',
46 | CACHE_TTL_SEC: parseInt(CACHE_TTL_SEC ?? '300'),
47 | CACHE_MAX_ITEMS: parseInt(CACHE_MAX_ITEMS ?? '10000'),
48 | CACHE_MAX_SIZE_MB: parseInt(CACHE_MAX_SIZE_MB ?? '1024'),
49 | };
50 |
51 | export default conf;
--------------------------------------------------------------------------------
/source/new-image-handler/src/debug.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'os';
2 | import { LRUCache } from 'lru-cache';
3 | import * as sharp from 'sharp';
4 |
5 | export interface ISharpInfo {
6 | cache: sharp.CacheResult;
7 | simd: boolean;
8 | counters: sharp.SharpCounters;
9 | concurrency: number;
10 | versions: {
11 | vips: string;
12 | cairo?: string;
13 | croco?: string;
14 | exif?: string;
15 | expat?: string;
16 | ffi?: string;
17 | fontconfig?: string;
18 | freetype?: string;
19 | gdkpixbuf?: string;
20 | gif?: string;
21 | glib?: string;
22 | gsf?: string;
23 | harfbuzz?: string;
24 | jpeg?: string;
25 | lcms?: string;
26 | orc?: string;
27 | pango?: string;
28 | pixman?: string;
29 | png?: string;
30 | svg?: string;
31 | tiff?: string;
32 | webp?: string;
33 | avif?: string;
34 | heif?: string;
35 | xml?: string;
36 | zlib?: string;
37 | };
38 | }
39 |
40 | export interface IDebugInfo {
41 | os: {
42 | arch: string;
43 | cpus: number;
44 | loadavg: number[];
45 | };
46 | memory: {
47 | stats: string;
48 | free: number;
49 | total: number;
50 | usage: NodeJS.MemoryUsage;
51 | };
52 | resource: {
53 | usage: NodeJS.ResourceUsage;
54 | };
55 | lruCache?: {
56 | keys: number;
57 | sizeMB: number;
58 | ttlSec: number;
59 | };
60 | sharp: ISharpInfo;
61 | }
62 |
63 | export default function debug(lruCache?: LRUCache): IDebugInfo {
64 | const ret: IDebugInfo = {
65 | os: {
66 | arch: os.arch(),
67 | cpus: os.cpus().length,
68 | loadavg: os.loadavg(),
69 | },
70 | memory: {
71 | stats: `free: ${formatBytes(os.freemem())}, total: ${formatBytes(os.totalmem())}, usage ${((os.totalmem() - os.freemem()) / os.totalmem() * 100).toFixed(2)} %`,
72 | free: os.freemem(),
73 | total: os.totalmem(),
74 | usage: process.memoryUsage(),
75 | },
76 | resource: {
77 | usage: process.resourceUsage(),
78 | },
79 | sharp: {
80 | cache: sharp.cache(),
81 | simd: sharp.simd(),
82 | counters: sharp.counters(),
83 | concurrency: sharp.concurrency(),
84 | versions: sharp.versions,
85 | },
86 | };
87 | if (lruCache) {
88 | ret.lruCache = {
89 | keys: lruCache.size,
90 | sizeMB: Math.round(b2mb(lruCache.calculatedSize) * 100) / 100,
91 | ttlSec: Math.round(lruCache.ttl / 1000),
92 | };
93 | }
94 | return ret;
95 | }
96 |
97 | function b2mb(v: number) {
98 | return v / 1048576;
99 | }
100 |
101 | function formatBytes(bytes: number) {
102 | const units = ['B', 'KB', 'MB', 'GB', 'TB'];
103 | let i = 0;
104 | for (; bytes >= 1024 && i < units.length - 1; i++) {
105 | bytes /= 1024;
106 | }
107 | return `${bytes.toFixed(2)} ${units[i]}`;
108 | };
109 |
--------------------------------------------------------------------------------
/source/new-image-handler/src/default.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import { ParsedUrlQuery } from 'querystring';
3 | import config from './config';
4 | import { InvalidArgument, IProcessor } from './processor';
5 | import { ImageProcessor } from './processor/image';
6 | import { StyleProcessor } from './processor/style';
7 | import { VideoProcessor } from './processor/video';
8 | import { IBufferStore, S3Store, LocalStore, MemKVStore, DynamoDBStore, IKVStore } from './store';
9 | import * as style from './style.json';
10 |
11 | const PROCESSOR_MAP: { [key: string]: IProcessor } = {
12 | [ImageProcessor.getInstance().name]: ImageProcessor.getInstance(),
13 | [StyleProcessor.getInstance().name]: StyleProcessor.getInstance(kvstore()),
14 | [VideoProcessor.getInstance().name]: VideoProcessor.getInstance(),
15 | };
16 |
17 | export function setMaxGifSizeMB(value: number) {
18 | ImageProcessor.getInstance().setMaxGifSizeMB(value);
19 | }
20 |
21 | export function setMaxGifPages(value: number) {
22 | ImageProcessor.getInstance().setMaxGifPages(value);
23 | }
24 |
25 | export function getProcessor(name: string): IProcessor {
26 | const processor = PROCESSOR_MAP[name];
27 | if (!processor) {
28 | throw new InvalidArgument('Can Not find processor');
29 | }
30 | return processor;
31 | }
32 |
33 | export function bufferStore(p?: string): IBufferStore {
34 | if (config.isProd) {
35 | if (!p) { p = config.srcBucket; }
36 | console.log(`use ${S3Store.name} s3://${p}`);
37 | return new S3Store(p);
38 | } else {
39 | if (!p) { p = path.join(__dirname, '../test/fixtures'); }
40 | console.log(`use ${LocalStore.name} file://${p}`);
41 | return new LocalStore(p);
42 | }
43 | }
44 |
45 | export function kvstore(): IKVStore {
46 | if (config.isProd) {
47 | console.log(`use ${DynamoDBStore.name}`);
48 | return new DynamoDBStore(config.styleTableName);
49 | } else {
50 | console.log(`use ${MemKVStore.name}`);
51 | return new MemKVStore(style);
52 | }
53 | }
54 |
55 | export function parseRequest(uri: string, query: ParsedUrlQuery): { uri: string; actions: string[] } {
56 | uri = uri.replace(/^\//, ''); // trim leading slash "/"
57 | const parts = uri.split(/@?!/, 2);
58 | if (parts.length === 1) {
59 | const x_oss_process = (query['x-oss-process'] as string) ?? '';
60 | return {
61 | uri: uri,
62 | actions: x_oss_process.split('/').filter(x => x),
63 | };
64 | }
65 | const stylename = (parts[1] ?? '').trim();
66 | if (!stylename) {
67 | throw new InvalidArgument('Empty style name');
68 | }
69 | return {
70 | uri: parts[0],
71 | actions: ['style', stylename],
72 | };
73 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/imagemagick.ts:
--------------------------------------------------------------------------------
1 | import * as child_process from 'child_process';
2 | import { Readable } from 'stream';
3 |
4 | // TODO: ImageMagick is slower than sharp for about 3x. Try removing ImageMagick later.
5 |
6 | const MAX_BUFFER = 1024 * 1024;
7 |
8 | // https://sourcegraph.com/github.com/nodejs/node@f7668fa2aa2781dc57d5423a0cfcfa933539779e/-/blob/lib/child_process.js?L279:10
9 | function _imagemagick(cmd: string, buffer: Buffer, args: readonly string[]) {
10 | const child = child_process.spawn(cmd, args);
11 |
12 | Readable.from(buffer).pipe(child.stdin);
13 |
14 | return new Promise((resolve, reject) => {
15 | const _stdout: any[] = [];
16 | let stdoutLen = 0;
17 |
18 | let killed = false;
19 | let exited = false;
20 | let ex: Error | null = null;
21 |
22 | function exithandler(code: number | null, signal: NodeJS.Signals | null) {
23 | if (exited) { return; }
24 | exited = true;
25 |
26 | // merge chunks
27 | const stdout = Buffer.concat(_stdout);
28 |
29 | if (!ex && code === 0 && signal === null) {
30 | resolve(stdout);
31 | return;
32 | }
33 |
34 | const _cmd = cmd + args.join(' ');
35 | if (!ex) {
36 | // eslint-disable-next-line no-restricted-syntax
37 | ex = new Error('Command failed: ' + _cmd + '\n');
38 | (ex as any).killed = child.killed || killed;
39 | (ex as any).code = code;
40 | (ex as any).signal = signal;
41 | }
42 | (ex as any).cmd = _cmd;
43 | reject(ex);
44 | }
45 |
46 | function errorhandler(e: Error) {
47 | ex = e;
48 | if (child.stdout) {
49 | child.stdout.destroy();
50 | }
51 | if (child.stderr) {
52 | child.stderr.destroy();
53 | }
54 | exithandler(null, null);
55 | }
56 |
57 | function kill() {
58 | if (child.stdout) {
59 | child.stdout.destroy();
60 | }
61 | if (child.stderr) {
62 | child.stderr.destroy();
63 | }
64 |
65 | killed = true;
66 | try {
67 | child.kill('SIGTERM');
68 | } catch (e) {
69 | ex = e as Error;
70 | exithandler(null, null);
71 | }
72 | }
73 |
74 | if (child.stdout) {
75 | child.stdout.on('data', function onChildStdout(chunk) {
76 | stdoutLen += chunk.length;
77 | if (stdoutLen > MAX_BUFFER) {
78 | ex = new Error('Exceed max buffer size');
79 | kill();
80 | } else {
81 | _stdout.push(chunk);
82 | }
83 | });
84 | } else {
85 | reject(new Error('Can\'t create stdout'));
86 | return;
87 | }
88 |
89 | child.on('close', exithandler);
90 | child.on('error', errorhandler);
91 | });
92 | }
93 |
94 | export function convert(buffer: Buffer, args: readonly string[]) {
95 | return _imagemagick('convert', buffer, ['-', ...args, '-']);
96 | }
97 |
98 | export function identify(buffer: Buffer, args: readonly string[]) {
99 | return _imagemagick('identify', buffer, [...args, '-']);
100 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/index-lambda.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-unresolved
2 | import { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from 'aws-lambda';
3 | import * as HttpErrors from 'http-errors';
4 | import config from './config';
5 | import debug from './debug';
6 | import { bufferStore, getProcessor, parseRequest } from './default';
7 | import * as is from './is';
8 | import { Features } from './processor';
9 |
10 |
11 | export const handler = WrapError(async (event: APIGatewayProxyEventV2): Promise => {
12 | console.log('event:', JSON.stringify(event));
13 |
14 | if (event.rawPath === '/' || event.rawPath === '/ping') {
15 | return resp(200, 'ok');
16 | } else if (event.rawPath === '/_debug') {
17 | console.log(JSON.stringify(debug()));
18 | return resp(400, 'Please check your server logs for more details!');
19 | }
20 |
21 | const accept = event.headers.Accept ?? event.headers.accept ?? '';
22 | const autoWebp = config.autoWebp && accept.includes('image/webp');
23 |
24 | console.log('autoWebp:', autoWebp);
25 |
26 | const bs = getBufferStore(event);
27 | const { uri, actions } = parseRequest(event.rawPath, event.queryStringParameters ?? {});
28 |
29 | if (actions.length > 1) {
30 | const processor = getProcessor(actions[0]);
31 | const context = await processor.newContext(uri, actions, bs);
32 | context.features[Features.AutoWebp] = autoWebp;
33 | const { data, type } = await processor.process(context);
34 |
35 | return resp(200, data, type, context.headers);
36 | } else {
37 | const { buffer, type, headers } = await bs.get(uri, bypass);
38 |
39 | return resp(200, buffer, type, headers);
40 | }
41 | });
42 |
43 | function bypass() {
44 | // NOTE: This is intended to tell CloudFront to directly access the s3 object without through API GW.
45 | throw new HttpErrors[403]('Please visit s3 directly');
46 | }
47 |
48 | function resp(code: number, body: any, type?: string, headers?: { [key: string]: any }): APIGatewayProxyResultV2 {
49 | const isBase64Encoded = Buffer.isBuffer(body);
50 | let data: string = '';
51 | if (isBase64Encoded) {
52 | data = body.toString('base64');
53 | } else if (is.string(body)) {
54 | data = body;
55 | type = 'text/plain';
56 | } else {
57 | data = JSON.stringify(body);
58 | type = 'application/json';
59 | }
60 |
61 | return {
62 | isBase64Encoded,
63 | statusCode: code,
64 | headers: Object.assign({ 'Content-Type': type ?? 'text/plain' }, headers),
65 | body: data,
66 | };
67 | }
68 |
69 | interface LambdaHandlerFn {
70 | (event: APIGatewayProxyEventV2): Promise;
71 | }
72 |
73 |
74 | function WrapError(fn: LambdaHandlerFn): LambdaHandlerFn {
75 | return async (event: APIGatewayProxyEventV2): Promise => {
76 | try {
77 | return await fn(event);
78 | } catch (err: any) {
79 | console.error(err);
80 | // ENOENT support
81 | if (err.code === 'ENOENT') {
82 | err.status = 404;
83 | err.message = 'NotFound';
84 | }
85 | const statusCode = err.statusCode ?? err.status ?? 500;
86 | const body = {
87 | status: statusCode,
88 | name: err.name,
89 | message: err.message,
90 | };
91 | return resp(statusCode, body);
92 | }
93 | };
94 | }
95 |
96 | const DefaultBufferStore = bufferStore();
97 |
98 | function getBufferStore(event: APIGatewayProxyEventV2) {
99 | const bucket = event.headers['x-bucket'];
100 | if (bucket) {
101 | return bufferStore(bucket);
102 | }
103 | return DefaultBufferStore;
104 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/is.ts:
--------------------------------------------------------------------------------
1 | export function inRange(val: number, min: number, max: number): boolean {
2 | return val >= min && val <= max;
3 | };
4 |
5 | export function hexColor(c: string): boolean {
6 | const regex = /^#([\da-f]{3}){1,2}$|^#([\da-f]{4}){1,2}$/i;
7 | return !!(c && regex.test(c));
8 | }
9 |
10 |
11 | export function defined(val: any) {
12 | return typeof val !== 'undefined' && val !== null;
13 | };
14 |
15 |
16 | export function object(val: any) {
17 | return typeof val === 'object';
18 | };
19 |
20 |
21 | export function plainObject(val: any) {
22 | return Object.prototype.toString.call(val) === '[object Object]';
23 | };
24 |
25 |
26 | export function fn(val: any) {
27 | return typeof val === 'function';
28 | };
29 |
30 |
31 | export function bool(val: any) {
32 | return typeof val === 'boolean';
33 | };
34 |
35 |
36 | export function buffer(val: any) {
37 | return val instanceof Buffer;
38 | };
39 |
40 |
41 | export function typedArray(val: any) {
42 | if (defined(val)) {
43 | switch (val.constructor) {
44 | case Uint8Array:
45 | case Uint8ClampedArray:
46 | case Int8Array:
47 | case Uint16Array:
48 | case Int16Array:
49 | case Uint32Array:
50 | case Int32Array:
51 | case Float32Array:
52 | case Float64Array:
53 | return true;
54 | }
55 | }
56 |
57 | return false;
58 | };
59 |
60 |
61 | export function string(val: any) {
62 | return typeof val === 'string' && val.length > 0;
63 | };
64 |
65 |
66 | export function number(val: any) {
67 | return typeof val === 'number' && !Number.isNaN(val);
68 | };
69 |
70 |
71 | export function integer(val: any) {
72 | return Number.isInteger(val);
73 | };
74 |
75 | export function inArray(val: any, list: any[]) {
76 | return list.includes(val);
77 | };
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/_base.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IAction, IActionOpts, IProcessContext, ReadOnly, IActionMask } from '..';
3 |
4 |
5 | export abstract class BaseImageAction implements IAction {
6 | public name: string = 'unknown';
7 | abstract validate(params: string[]): ReadOnly;
8 | abstract process(ctx: IProcessContext, params: string[]): Promise;
9 | public beforeNewContext(_1: IProcessContext, params: string[], _3: number): void {
10 | this.validate(params);
11 | }
12 | public beforeProcess(_1: IImageContext, _2: string[], _3: number): void { }
13 | }
14 |
15 | export class ActionMask implements IActionMask {
16 | private readonly _masks: boolean[];
17 |
18 | public constructor(private readonly _actions: string[]) {
19 | this._masks = _actions.map(() => true);
20 | }
21 |
22 | public get length(): number {
23 | return this._actions.length;
24 | }
25 |
26 | private _check(index: number): void {
27 | if (!(0 <= index && index < this.length)) {
28 | throw new Error('Index out of range');
29 | }
30 | }
31 |
32 | public getAction(index: number): string {
33 | this._check(index);
34 | return this._actions[index];
35 | }
36 |
37 | public isEnabled(index: number): boolean {
38 | this._check(index);
39 | return this._masks[index];
40 | }
41 |
42 | public isDisabled(index: number): boolean {
43 | this._check(index);
44 | return !this._masks[index];
45 | }
46 |
47 | public enable(index: number) {
48 | this._check(index);
49 | this._masks[index] = true;
50 | }
51 |
52 | public disable(index: number) {
53 | this._check(index);
54 | this._masks[index] = false;
55 | }
56 |
57 | public disableAll() {
58 | for (let i = 0; i < this._masks.length; i++) {
59 | this._masks[i] = false;
60 | }
61 | }
62 |
63 | public filterEnabledActions(): string[] {
64 | return this._actions.filter((_, index) => this._masks[index]);
65 | }
66 |
67 | public forEachAction(cb: (action: string, enabled: boolean, index: number) => void): void {
68 | this._actions.forEach((action, index) => {
69 | cb(action, this.isEnabled(index), index);
70 | });
71 | }
72 | }
73 |
74 | export function split1(s: string, sep: string = ',') {
75 | const split = s.split(sep, 1);
76 | return [split[0], s.substring(split[0].length + sep.length)];
77 | }
78 |
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/auto-orient.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument, IProcessContext, Features } from '..';
3 | import { BaseImageAction } from './_base';
4 |
5 | export interface AutoOrientOpts extends IActionOpts {
6 | auto: boolean;
7 | }
8 |
9 | export class AutoOrientAction extends BaseImageAction {
10 | public readonly name: string = 'auto-orient';
11 |
12 | public beforeNewContext(ctx: IProcessContext, _: string[]): void {
13 | ctx.features[Features.AutoOrient] = false;
14 | }
15 |
16 | public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
17 | if ('gif' === ctx.metadata.format) {
18 | ctx.mask.disable(index);
19 | }
20 | }
21 |
22 | public validate(params: string[]): ReadOnly {
23 | const opt: AutoOrientOpts = { auto: false };
24 |
25 | if (params.length !== 2) {
26 | throw new InvalidArgument('Auto-orient param error, e.g: auto-orient,1');
27 | }
28 | if (params[1] === '1') {
29 | opt.auto = true;
30 | } else if (params[1] === '0') {
31 | opt.auto = false;
32 | } else {
33 | throw new InvalidArgument('Auto-orient param must be 0 or 1');
34 | }
35 | return opt;
36 | }
37 |
38 |
39 | public async process(ctx: IImageContext, params: string[]): Promise {
40 | const opt = this.validate(params);
41 | if (opt.auto) {
42 | ctx.image.rotate();
43 | }
44 |
45 | }
46 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/blur.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import * as is from '../../is';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface BlurOpts extends IActionOpts {
7 | r: number;
8 | s: number;
9 | }
10 |
11 | export class BlurAction extends BaseImageAction {
12 | public readonly name: string = 'blur';
13 |
14 | public validate(params: string[]): ReadOnly {
15 | let opt: BlurOpts = { r: 0, s: 0 };
16 |
17 | if (params.length < 2) {
18 | throw new InvalidArgument('blur param error, e.g: blur,r_3,s_2');
19 | }
20 |
21 | for (const param of params) {
22 | if ((this.name === param) || (!param)) {
23 | continue;
24 | }
25 | const [k, v] = param.split('_');
26 | if (k === 'r') {
27 | const r = Number.parseInt(v, 10);
28 | if (is.inRange(r, 1, 50)) {
29 | opt.r = r;
30 | } else {
31 | throw new InvalidArgument('Blur param \'r\' must be between 0 and 50');
32 | }
33 | } else if (k === 's') {
34 | const s = Number.parseInt(v, 10);
35 | if (is.inRange(s, 1, 50)) {
36 | opt.s = s;
37 | } else {
38 | throw new InvalidArgument('Blur param \'s\' must be between 0 and 50');
39 | }
40 | } else {
41 | throw new InvalidArgument(`Unkown param: "${k}"`);
42 | }
43 |
44 | }
45 | return opt;
46 | }
47 |
48 |
49 | public async process(ctx: IImageContext, params: string[]): Promise {
50 | const opt = this.validate(params);
51 | const a = -0.0057;
52 | const b = 1.1787;
53 | const c = -0.0694;
54 | const sigma = a * opt.s * opt.s + b * opt.s + c;
55 |
56 | const sqrtln01 = 1.51743; // Sqrt(-ln(0.1))
57 | const max_x = Math.floor(sigma * sqrtln01);
58 | const max_n = 2 * Math.max(max_x - 1, 0) + 1; // The max gauss kernel size
59 | const n = 2 * opt.r + 1; // The given gauss kernel size
60 |
61 | if ((n < max_n) && (n <= 51)) { // It will be really slow if n > 51
62 | console.log('Use manual blur');
63 | ctx.image.convolve({
64 | width: n,
65 | height: n,
66 | kernel: gaussmat(n, sigma),
67 | });
68 | } else {
69 | console.log('Use built-in blur');
70 | ctx.image.blur(sigma);
71 | }
72 | }
73 | }
74 |
75 | function gaussmat(n: number, sigma: number): ArrayLike {
76 | if (n % 2 === 0) {
77 | throw new Error('gaussmat kernel size must be odd');
78 | }
79 | const mat = new Array(n * n);
80 | for (let y = 0; y < n; y++) {
81 | for (let x = 0; x < n; x++) {
82 | // eslint-disable-next-line no-bitwise
83 | let xo = x - (n >> 1);
84 | // eslint-disable-next-line no-bitwise
85 | let yo = y - (n >> 1);
86 | const distance = xo * xo + yo * yo;
87 | const v = Math.exp(-distance / (sigma * sigma));
88 | mat[y * n + x] = v;
89 | }
90 | }
91 | return mat;
92 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/bright.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import * as is from '../../is';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface BrightOpts extends IActionOpts {
7 | bright: number;
8 | }
9 |
10 | export class BrightAction extends BaseImageAction {
11 | public readonly name: string = 'bright';
12 |
13 | public validate(params: string[]): ReadOnly {
14 | const opt: BrightOpts = { bright: 100 };
15 |
16 | if (params.length !== 2) {
17 | throw new InvalidArgument('Bright param error, e.g: bright,50');
18 | }
19 | const b = Number.parseInt(params[1], 10);
20 | if (is.inRange(b, -100, 100)) {
21 | opt.bright = b;
22 | } else {
23 | throw new InvalidArgument('Bright must be between -100 and 100');
24 | }
25 | return opt;
26 | }
27 |
28 |
29 | public async process(ctx: IImageContext, params: string[]): Promise {
30 | const opt = this.validate(params);
31 |
32 | ctx.image.linear(1, opt.bright);
33 | }
34 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/cgif.ts:
--------------------------------------------------------------------------------
1 | import { IActionOpts, ReadOnly, Features, IProcessContext, InvalidArgument } from '..';
2 | import * as is from '../../is';
3 | import { BaseImageAction } from './_base';
4 |
5 |
6 | export interface CgifOpts extends IActionOpts {
7 | s?: number;
8 | }
9 |
10 | export class CgifAction extends BaseImageAction {
11 | public readonly name: string = 'cgif';
12 |
13 | public beforeNewContext(ctx: IProcessContext, params: string[]): void {
14 | ctx.features[Features.ReadAllAnimatedFrames] = false;
15 | if (params.length !== 2) {
16 | throw new InvalidArgument('Cut gif param error, e.g: cgif,s_1');
17 | }
18 | const [k, v] = params[1].split('_');
19 | if (k === 's') {
20 | if (!is.inRange(Number.parseInt(v, 10), 1, 1000)) {
21 | throw new InvalidArgument(`Unkown param: "${k}"`);
22 | }
23 | ctx.features[Features.LimitAnimatedFrames] = Number.parseInt(v, 10);
24 | } else {
25 | throw new InvalidArgument(`Unkown param: "${k}"`);
26 | }
27 | }
28 |
29 | public validate(): ReadOnly {
30 | let opt: CgifOpts = {};
31 | return opt;
32 | }
33 |
34 | public async process(): Promise {
35 | }
36 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/circle.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
4 | import * as is from '../../is';
5 | import { BaseImageAction } from './_base';
6 |
7 | export interface CircleOpts extends IActionOpts {
8 | r: number;
9 | }
10 |
11 | export class CircleAction extends BaseImageAction {
12 | public readonly name: string = 'circle';
13 |
14 | public validate(params: string[]): ReadOnly {
15 | let opt: CircleOpts = { r: 1 };
16 |
17 | if (params.length !== 2) {
18 | throw new InvalidArgument('Circle param error, e.g: circle,r_30');
19 | }
20 |
21 | for (const param of params) {
22 | if ((this.name === param) || (!param)) {
23 | continue;
24 | }
25 | const [k, v] = param.split('_');
26 | if (k === 'r') {
27 | const r = Number.parseInt(v, 10);
28 | if (is.inRange(r, 1, 4096)) {
29 | opt.r = r;
30 | } else {
31 | throw new InvalidArgument('Circle param \'r\' must be between 1 and 4096');
32 | }
33 | } else {
34 | throw new InvalidArgument(`Unkown param: "${k}"`);
35 | }
36 | }
37 |
38 | return opt;
39 | }
40 |
41 |
42 | public async process(ctx: IImageContext, params: string[]): Promise {
43 | const opt = this.validate(params);
44 | const metadata = await sharp(await ctx.image.toBuffer()).metadata(); // https://github.com/lovell/sharp/issues/2959
45 | if (!(metadata.width && metadata.height)) {
46 | throw new InvalidArgument('Can\'t read image\'s width and height');
47 | }
48 |
49 | const pages = metadata.pages ?? 1;
50 | const cx = metadata.width / 2;
51 | const cy = metadata.height / 2;
52 | const s = Math.min(metadata.width, metadata.height); // shorter side
53 | const r = Math.min(opt.r, s / 2); // radius
54 | const d = Math.min(Math.round(2 * r) + 1, s); // diameter
55 |
56 | const circles = Array.from({ length: pages }, (_, i) =>
57 | ``,
58 | );
59 | const mask = Buffer.from(``);
62 |
63 | const region = {
64 | left: Math.max(Math.round(cx - r), 0),
65 | top: Math.max(Math.round(cy - r), 0),
66 | width: d,
67 | height: d,
68 | };
69 |
70 | ctx.image.extract(region).composite([
71 | { input: mask, blend: 'dest-in' },
72 | ]);
73 | }
74 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/contrast.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import * as is from '../../is';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface ContrastOpts extends IActionOpts {
7 | contrast: number;
8 | }
9 |
10 | export class ContrastAction extends BaseImageAction {
11 | public readonly name: string = 'contrast';
12 |
13 | public validate(params: string[]): ReadOnly {
14 | const opt: ContrastOpts = { contrast: -100 };
15 |
16 | if (params.length !== 2) {
17 | throw new InvalidArgument('Contrast param error, e.g: contrast,-50');
18 | }
19 | const b = Number.parseInt(params[1], 10);
20 | if (is.inRange(b, -100, 100)) {
21 | opt.contrast = b;
22 | } else {
23 | throw new InvalidArgument('Contrast must be between -100 and 100');
24 | }
25 | return opt;
26 | }
27 |
28 |
29 | public async process(ctx: IImageContext, params: string[]): Promise {
30 | const opt = this.validate(params);
31 |
32 | if (opt.contrast > 0) {
33 | ctx.image.linear((2 * opt.contrast + 100) / 200 + 0.5);
34 | } else {
35 | ctx.image.linear((opt.contrast + 100) / 200 + 0.5);
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/crop.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface CropOpts extends IActionOpts {
7 | w: number;
8 | h: number;
9 | x: number;
10 | y: number;
11 | g: 'nw' | 'north' | 'ne' | 'west' | 'center' | 'east' | 'sw' | 'south' | 'se';
12 | }
13 |
14 | export class CropAction extends BaseImageAction {
15 | public readonly name: string = 'crop';
16 |
17 | public validate(params: string[]): ReadOnly {
18 | let opt: CropOpts = { w: 0, h: 0, x: 0, y: 0, g: 'nw' };
19 |
20 | if (params.length < 2) {
21 | throw new InvalidArgument('Crop param error, e.g: crop,x_100,y_50');
22 | }
23 |
24 | for (const param of params) {
25 | if ((this.name === param) || (!param)) {
26 | continue;
27 | }
28 | const [k, v] = param.split('_');
29 | if (k === 'w') {
30 | const w = Number.parseInt(v, 10);
31 | if (w > 0) {
32 | opt.w = w;
33 | } else {
34 | throw new InvalidArgument('Crop param w must be greater than 0');
35 | }
36 | } else if (k === 'h') {
37 | const h = Number.parseInt(v, 10);
38 | if (h > 0) {
39 | opt.h = h;
40 | } else {
41 | throw new InvalidArgument('Crop param h must be greater than 0');
42 | }
43 | } else if (k === 'x') {
44 | const x = Number.parseInt(v, 10);
45 | if (x >= 0) {
46 | opt.x = x;
47 | } else {
48 | throw new InvalidArgument('Crop param x must be greater than or equal to 0');
49 | }
50 | } else if (k === 'y') {
51 | const y = Number.parseInt(v, 10);
52 | if (y >= 0) {
53 | opt.y = y;
54 | } else {
55 | throw new InvalidArgument('Crop param y must be greater than or equal to 0');
56 | }
57 | } else if (k === 'g') {
58 | if (v === 'nw' || v === 'north' || v === 'ne' ||
59 | v === 'west' || v === 'center' || v === 'east' ||
60 | v === 'sw' || v === 'south' || v === 'se') {
61 | opt.g = v;
62 | } else {
63 | throw new InvalidArgument('Crop param g must be one of nw, north, ne, west, center, east, sw, south, se.');
64 | }
65 | } else {
66 | throw new InvalidArgument(`Unkown param: "${k}"`);
67 | }
68 |
69 | }
70 | return opt;
71 | }
72 |
73 |
74 | public async process(ctx: IImageContext, params: string[]): Promise {
75 | const opt = this.validate(params);
76 |
77 | let height = opt.h;
78 | let width = opt.w;
79 | let x = opt.x;
80 | let y = opt.y;
81 |
82 | const metadata = await sharp(await ctx.image.toBuffer()).metadata();
83 | if (metadata.height === undefined || metadata.width === undefined) {
84 | throw new InvalidArgument('Incorrect image format');
85 | }
86 |
87 | if (opt.g === 'west' || opt.g === 'center' || opt.g === 'east') {
88 | y += Math.round(metadata.height / 3);
89 | } else if (opt.g === 'sw' || opt.g === 'south' || opt.g === 'se') {
90 | y += Math.round(metadata.height / 3) * 2;
91 | }
92 |
93 | if (opt.g === 'north' || opt.g === 'center' || opt.g === 'south') {
94 | x += Math.round(metadata.width / 3);
95 | } else if (opt.g === 'ne' || opt.g === 'east' || opt.g === 'se') {
96 | x += Math.round(metadata.width / 3) * 2;
97 | }
98 |
99 | if (x < 0 || x >= metadata.width) {
100 | throw new InvalidArgument(`Incorrect crop param, x value must be in [0, ${metadata.width}] `);
101 | }
102 | if (y < 0 || y >= metadata.height) {
103 | throw new InvalidArgument(`Incorrect crop param, y value must be in [0, ${metadata.height}] `);
104 | }
105 |
106 | // The width and height are not set in the parameter, modify them to reasonable values
107 | if (width === 0) {
108 | width = metadata.width - x;
109 | }
110 | if (height === 0) {
111 | height = metadata.height - y;
112 | }
113 |
114 | // The width and height of the set parameters exceed the size of the picture, modify them to reasonable values
115 | if (x + width > metadata.width) {
116 | width = metadata.width - x;
117 | }
118 | if (y + height > metadata.height) {
119 | height = metadata.height - y;
120 | }
121 |
122 | ctx.image = sharp(await ctx.image.extract({
123 | left: x,
124 | top: y,
125 | width: width,
126 | height: height,
127 | }).toBuffer(), { animated: true });
128 | }
129 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/format.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument, Features, IProcessContext } from '..';
3 | import { BaseImageAction } from './_base';
4 |
5 | export interface FormatOpts extends IActionOpts {
6 | format: string;
7 | }
8 |
9 | export class FormatAction extends BaseImageAction {
10 | public readonly name: string = 'format';
11 |
12 | public beforeNewContext(ctx: IProcessContext, params: string[]): void {
13 | const opts = this.validate(params);
14 | if (['webp', 'gif'].includes(opts.format)) {
15 | ctx.features[Features.ReadAllAnimatedFrames] = true;
16 | } else {
17 | ctx.features[Features.ReadAllAnimatedFrames] = false;
18 | }
19 | }
20 |
21 | public beforeProcess(ctx: IImageContext, params: string[], index: number): void {
22 | const opts = this.validate(params);
23 | if (('gif' === ctx.metadata.format) && ('gif' === opts.format)) {
24 | ctx.mask.disable(index);
25 | }
26 | }
27 |
28 | public validate(params: string[]): ReadOnly {
29 | let opt: FormatOpts = { format: '' };
30 |
31 | if (params.length !== 2) {
32 | throw new InvalidArgument(`Format param error, e.g: format,jpg (${SUPPORTED_FORMAT.toString()})`);
33 | }
34 | opt.format = params[1];
35 |
36 | if (!SUPPORTED_FORMAT.includes(opt.format)) {
37 | throw new InvalidArgument(`Format must be one of ${SUPPORTED_FORMAT.toString()}`);
38 | }
39 |
40 | return opt;
41 | }
42 |
43 |
44 | public async process(ctx: IImageContext, params: string[]): Promise {
45 | if (ctx.features[Features.AutoWebp]) {
46 | ctx.features[Features.AutoWebp] = false;
47 | }
48 |
49 | const opt = this.validate(params);
50 | if ('gif' === opt.format) {
51 | return; // nothing to do
52 | }
53 | if (['jpeg', 'jpg'].includes(opt.format)) {
54 | ctx.metadata.format = 'jpeg';
55 | ctx.image.jpeg();
56 | } else if (opt.format === 'png') {
57 | ctx.metadata.format = 'png';
58 | ctx.image.png({ effort: 2, quality: 80 });
59 | } else if (opt.format === 'webp') {
60 | ctx.metadata.format = 'webp';
61 | ctx.image.webp({ effort: 2, quality: 80 });
62 | }
63 |
64 | }
65 | }
66 |
67 | const SUPPORTED_FORMAT = [
68 | 'jpg',
69 | 'jpeg',
70 | 'png',
71 | 'webp',
72 | 'gif',
73 | ];
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/grey.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import { BaseImageAction } from './_base';
4 |
5 | export interface GreyOpts extends IActionOpts {
6 | grey: boolean;
7 | }
8 |
9 | export class GreyAction extends BaseImageAction {
10 | public readonly name: string = 'grey';
11 |
12 | public validate(params: string[]): ReadOnly {
13 | let opt: GreyOpts = { grey: false };
14 |
15 | if (params.length !== 2) {
16 | throw new InvalidArgument('Grey param error, e.g: grey,1');
17 | }
18 | if (params[1] === '1') {
19 | opt.grey = true;
20 | } else if (params[1] === '0') {
21 | opt.grey = false;
22 |
23 | } else {
24 | throw new InvalidArgument('Grey must be 0 or 1');
25 | }
26 | return opt;
27 | }
28 |
29 |
30 | public async process(ctx: IImageContext, params: string[]): Promise {
31 | const opt = this.validate(params);
32 | if (opt.grey) {
33 | ctx.image.greyscale();
34 | }
35 |
36 | }
37 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/indexcrop.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface IndexCropOpts extends IActionOpts {
7 | x: number;
8 | y: number;
9 | i: number;
10 | }
11 |
12 | export class IndexCropAction extends BaseImageAction {
13 | public readonly name: string = 'indexcrop';
14 |
15 | public validate(params: string[]): ReadOnly {
16 | let opt: IndexCropOpts = { x: 0, y: 0, i: 0 };
17 |
18 | if (params.length < 3) {
19 | throw new InvalidArgument('IndexCrop param error, e.g: indexcrop,x_100,i_0');
20 | }
21 |
22 | for (const param of params) {
23 | if ((this.name === param) || (!param)) {
24 | continue;
25 | }
26 | const [k, v] = param.split('_');
27 | if (k === 'x') {
28 | opt.x = Number.parseInt(v, 10);
29 | if (opt.x < 0) {
30 | throw new InvalidArgument('Param error: x value must be greater than 0');
31 | }
32 | } else if (k === 'y') {
33 | opt.y = Number.parseInt(v, 10);
34 | if (opt.y < 0) {
35 | throw new InvalidArgument('Param error: y value must be greater than 0');
36 | }
37 | } else if (k === 'i') {
38 | opt.i = Number.parseInt(v, 10);
39 | } else {
40 | throw new InvalidArgument(`Unkown param: "${k}"`);
41 | }
42 | }
43 | if (opt.x > 0 && opt.y > 0) {
44 | throw new InvalidArgument('Param error: Cannot enter x and y at the same time');
45 | }
46 |
47 | return opt;
48 | }
49 |
50 |
51 | public async process(ctx: IImageContext, params: string[]): Promise {
52 | const opt = this.validate(params);
53 |
54 | let x = 0;
55 | let y = 0;
56 | let w = 0;
57 | let h = 0;
58 | let needCrop: boolean = true;
59 |
60 | const metadata = await sharp(await ctx.image.toBuffer()).metadata();
61 | if (metadata.height === undefined || metadata.width === undefined) {
62 | throw new InvalidArgument('Incorrect image format');
63 | }
64 |
65 |
66 | if (metadata.height === undefined || metadata.width === undefined) {
67 | throw new InvalidArgument('Incorrect image format');
68 | }
69 | h = metadata.height;
70 | w = metadata.width;
71 |
72 | if (opt.x > 0) {
73 | if (opt.x > metadata.width) {
74 | needCrop = false;
75 | return;
76 | }
77 | const count = Math.floor(metadata.width / opt.x);
78 | if (opt.i + 1 > count) {
79 | needCrop = false;
80 | return;
81 | }
82 | x = opt.i * opt.x;
83 | w = opt.x;
84 |
85 | } else if (opt.y > 0) {
86 |
87 | if (opt.y > metadata.height) {
88 | needCrop = false;
89 | return;
90 | }
91 |
92 | const count = Math.floor(metadata.height / opt.y);
93 | if (opt.i + 1 > count) {
94 | needCrop = false;
95 | return;
96 | }
97 | y = opt.i * opt.y;
98 | h = opt.y;
99 |
100 | }
101 |
102 | if (needCrop) {
103 | ctx.image = sharp(await ctx.image.extract({
104 | left: x,
105 | top: y,
106 | width: w,
107 | height: h,
108 | }).toBuffer(), { animated: true });
109 | }
110 |
111 | }
112 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/info.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument, Features } from '..';
3 | import { BaseImageAction } from './_base';
4 |
5 |
6 | export class InfoAction extends BaseImageAction {
7 | public readonly name: string = 'info';
8 |
9 | public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
10 | ctx.mask.disableAll();
11 | ctx.mask.enable(index);
12 | }
13 |
14 | public validate(params: string[]): ReadOnly {
15 | if ((params.length !== 1) || (params[0] !== this.name)) {
16 | throw new InvalidArgument('Info param error');
17 | }
18 | return {};
19 | }
20 |
21 | public async process(ctx: IImageContext, params: string[]): Promise {
22 | this.validate(params);
23 |
24 | const metadata = await ctx.image.metadata();
25 | ctx.info = {
26 | FileSize: { value: String(metadata.size) },
27 | Format: { value: String(metadata.format === 'jpeg' ? 'jpg' : metadata.format) },
28 | ImageHeight: { value: String(metadata.pageHeight ?? metadata.height) },
29 | ImageWidth: { value: String(metadata.width) },
30 | };
31 |
32 | ctx.features[Features.ReturnInfo] = true;
33 | }
34 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/interlace.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import { BaseImageAction } from './_base';
4 | export interface InterlaceOpts extends IActionOpts {
5 | interlace: boolean;
6 | }
7 |
8 | export class InterlaceAction extends BaseImageAction {
9 | public readonly name: string = 'interlace';
10 |
11 | public validate(params: string[]): ReadOnly {
12 | let opt: InterlaceOpts = { interlace: false };
13 |
14 | if (params.length !== 2) {
15 | throw new InvalidArgument('Interlace param error, e.g: interlace,1');
16 | }
17 | if (params[1] === '1') {
18 | opt.interlace = true;
19 | } else if (params[1] === '0') {
20 | opt.interlace = false;
21 | } else {
22 | throw new InvalidArgument('Interlace must be 0 or 1');
23 | }
24 | return opt;
25 | }
26 |
27 | public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
28 | if ('gif' === ctx.metadata.format) {
29 | ctx.mask.disable(index);
30 | }
31 | }
32 |
33 | public async process(ctx: IImageContext, params: string[]): Promise {
34 | const opt = this.validate(params);
35 | const metadata = ctx.metadata;
36 | if (('jpg' === metadata.format || 'jpeg' === metadata.format) && opt.interlace) {
37 | ctx.image.jpeg({ progressive: true });
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/quality.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, InvalidArgument, ReadOnly } from '..';
4 | import * as is from '../../is';
5 | import { BaseImageAction } from './_base';
6 | import * as jpeg from './jpeg';
7 |
8 |
9 | const JPG = 'jpg';
10 | const JPEG = sharp.format.jpeg.id;
11 | const WEBP = sharp.format.webp.id;
12 |
13 | export interface QualityOpts extends IActionOpts {
14 | q?: number;
15 | Q?: number;
16 | }
17 |
18 | export class QualityAction extends BaseImageAction {
19 | public readonly name: string = 'quality';
20 |
21 | public beforeProcess(ctx: IImageContext, _2: string[], index: number): void {
22 | if ('gif' === ctx.metadata.format) {
23 | ctx.mask.disable(index);
24 | }
25 | }
26 |
27 | public validate(params: string[]): ReadOnly {
28 | const opt: QualityOpts = {};
29 | for (const param of params) {
30 | if ((this.name === param) || (!param)) {
31 | continue;
32 | }
33 | const [k, v] = param.split('_');
34 | if (k === 'q') {
35 | const q = Number.parseInt(v, 10);
36 | if (is.inRange(q, 1, 100)) {
37 | opt.q = q;
38 | } else {
39 | throw new InvalidArgument('Quality must be between 1 and 100');
40 | }
41 | } else if (k === 'Q') {
42 | const Q = Number.parseInt(v, 10);
43 | if (is.inRange(Q, 1, 100)) {
44 | opt.Q = Q;
45 | } else {
46 | throw new InvalidArgument('Quality must be between 1 and 100');
47 | }
48 | } else {
49 | throw new InvalidArgument(`Unkown param: "${k}"`);
50 | }
51 | }
52 | return opt;
53 | }
54 | public async process(ctx: IImageContext, params: string[]): Promise {
55 | const opt = this.validate(params);
56 | const metadata = ctx.metadata; // If the format is changed before.
57 | if (JPEG === metadata.format || JPG === metadata.format) {
58 | let q = 72;
59 | if (opt.q) {
60 | const buffer = await ctx.image.toBuffer();
61 | const estq = jpeg.decode(buffer).quality;
62 | q = Math.round(estq * opt.q / 100);
63 | } else if (opt.Q) {
64 | q = opt.Q;
65 | }
66 | ctx.image.jpeg({ quality: q });
67 | } else if (WEBP === metadata.format) {
68 | ctx.image.webp({ quality: (opt.q ?? opt.Q) });
69 | }
70 | }
71 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/resize.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, InvalidArgument, ReadOnly } from '..';
4 | import * as is from '../../is';
5 | import { BaseImageAction } from './_base';
6 |
7 | export const enum Mode {
8 | LFIT = 'lfit',
9 | MFIT = 'mfit',
10 | FILL = 'fill',
11 | PAD = 'pad',
12 | FIXED = 'fixed'
13 | }
14 |
15 | export interface ResizeOpts extends IActionOpts {
16 | m?: Mode;
17 | w?: number;
18 | h?: number;
19 | l?: number;
20 | s?: number;
21 | limit?: boolean;
22 | color?: string;
23 | p?: number;
24 | }
25 |
26 | export class ResizeAction extends BaseImageAction {
27 | public readonly name: string = 'resize';
28 |
29 | public validate(params: string[]): ReadOnly {
30 | const opt: ResizeOpts = {
31 | m: Mode.LFIT,
32 | limit: true,
33 | color: '#FFFFFF',
34 | };
35 | for (const param of params) {
36 | if ((this.name === param) || (!param)) {
37 | continue;
38 | }
39 | const [k, v] = param.split('_');
40 | if (k === 'w') {
41 | opt.w = Number.parseInt(v, 10);
42 | } else if (k === 'h') {
43 | opt.h = Number.parseInt(v, 10);
44 | } else if (k === 'l') {
45 | opt.l = Number.parseInt(v, 10);
46 | } else if (k === 's') {
47 | opt.s = Number.parseInt(v, 10);
48 | } else if (k === 'm') {
49 | if (v && ((v === Mode.LFIT) || (v === Mode.MFIT) || (v === Mode.FILL) || (v === Mode.PAD) || (v === Mode.FIXED))) {
50 | opt.m = v;
51 | } else {
52 | throw new InvalidArgument(`Unkown m: "${v}"`);
53 | }
54 | } else if (k === 'limit') {
55 | if (v && (v === '0' || v === '1')) {
56 | opt.limit = (v === '1');
57 | } else {
58 | throw new InvalidArgument(`Unkown limit: "${v}"`);
59 | }
60 | } else if (k === 'color') {
61 | const color = '#' + v;
62 | if (is.hexColor(color)) {
63 | opt.color = color;
64 | } else {
65 | throw new InvalidArgument(`Unkown color: "${v}"`);
66 | }
67 | } else if (k === 'p') {
68 | const p = Number.parseInt(v, 10);
69 | if (is.inRange(p, 1, 1000)) {
70 | opt.p = p;
71 | } else {
72 | throw new InvalidArgument(`Unkown p: "${v}"`);
73 | }
74 | } else {
75 | throw new InvalidArgument(`Unkown param: "${k}"`);
76 | }
77 | }
78 | return opt;
79 | }
80 |
81 | public beforeProcess(ctx: IImageContext, params: string[], index: number): void {
82 | const metadata = ctx.metadata;
83 | if ('gif' === metadata.format) {
84 | const opt = buildSharpOpt(ctx, this.validate(params));
85 | const isEnlargingWidth = (opt.width && metadata.width && opt.width > metadata.width);
86 | const isEnlargingHeight = (opt.height && metadata.pageHeight && (opt.height > metadata.pageHeight));
87 | if (isEnlargingWidth || isEnlargingHeight) {
88 | ctx.mask.disable(index);
89 | }
90 | }
91 | }
92 |
93 | public async process(ctx: IImageContext, params: string[]): Promise {
94 | const opt = buildSharpOpt(ctx, this.validate(params));
95 | ctx.image.resize(null, null, opt);
96 | }
97 | }
98 |
99 | function buildSharpOpt(ctx: IImageContext, o: ResizeOpts): sharp.ResizeOptions {
100 | const opt: sharp.ResizeOptions = {
101 | width: o.w,
102 | height: o.h,
103 | withoutEnlargement: o.limit,
104 | background: o.color,
105 | };
106 | // Mode
107 | if (o.m === Mode.LFIT) {
108 | opt.fit = sharp.fit.inside;
109 | } else if (o.m === Mode.MFIT) {
110 | opt.fit = sharp.fit.outside;
111 | } else if (o.m === Mode.FILL) {
112 | opt.fit = sharp.fit.cover;
113 | } else if (o.m === Mode.PAD) {
114 | opt.fit = sharp.fit.contain;
115 | } else if (o.m === Mode.FIXED) {
116 | opt.fit = sharp.fit.fill;
117 | }
118 | const metadata = ctx.metadata;
119 | if (!(metadata.width && metadata.height)) {
120 | throw new InvalidArgument('Can\'t read image\'s width and height');
121 | }
122 |
123 | if (o.p && (!o.w) && (!o.h)) {
124 | opt.withoutEnlargement = false;
125 | opt.width = Math.round(metadata.width * o.p * 0.01);
126 | } else {
127 | if (o.l) {
128 | if (metadata.width > metadata.height) {
129 | opt.width = o.l;
130 | } else {
131 | opt.height = o.l;
132 | }
133 | }
134 | if (o.s) {
135 | if (metadata.height < metadata.width) {
136 | opt.height = o.s;
137 | } else {
138 | opt.width = o.s;
139 | }
140 | }
141 | }
142 | return opt;
143 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/rotate.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
4 | import * as is from '../../is';
5 | import { BaseImageAction } from './_base';
6 |
7 | export interface RotateOpts extends IActionOpts {
8 | degree: number;
9 | }
10 |
11 | export class RotateAction extends BaseImageAction {
12 | public readonly name: string = 'rotate';
13 |
14 | public validate(params: string[]): ReadOnly {
15 | let opt: RotateOpts = { degree: 0 };
16 |
17 | if (params.length !== 2) {
18 | throw new InvalidArgument('Rotate param error, e.g: rotate,90');
19 | }
20 | const d = Number.parseInt(params[1], 10);
21 | if (is.inRange(d, 0, 360)) {
22 | opt.degree = d;
23 | } else {
24 | throw new InvalidArgument('Rotate must be between 0 and 360');
25 | }
26 | return opt;
27 | }
28 |
29 |
30 | public async process(ctx: IImageContext, params: string[]): Promise {
31 | const opt = this.validate(params);
32 | ctx.image = sharp(await ctx.image.toBuffer()).rotate(opt.degree, {
33 | background: '#ffffff',
34 | });
35 | }
36 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/rounded-corners.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { IImageContext } from '.';
3 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
4 | import * as is from '../../is';
5 | import { BaseImageAction } from './_base';
6 |
7 | export interface RoundedCornersOpts extends IActionOpts {
8 | r: number;
9 | }
10 |
11 | export class RoundedCornersAction extends BaseImageAction {
12 | public readonly name: string = 'rounded-corners';
13 |
14 | public validate(params: string[]): ReadOnly {
15 | let opt: RoundedCornersOpts = { r: 1 };
16 |
17 | if (params.length !== 2) {
18 | throw new InvalidArgument('RoundedCorners param error, e.g: rounded-corners,r_30');
19 | }
20 |
21 | for (const param of params) {
22 | if ((this.name === param) || (!param)) {
23 | continue;
24 | }
25 | const [k, v] = param.split('_');
26 | if (k === 'r') {
27 | const r = Number.parseInt(v, 10);
28 | if (is.inRange(r, 1, 4096)) {
29 | opt.r = r;
30 | } else {
31 | throw new InvalidArgument('RoundedCorners param \'r\' must be between 1 and 4096');
32 | }
33 | } else {
34 | throw new InvalidArgument(`Unkown param: "${k}"`);
35 | }
36 | }
37 |
38 | return opt;
39 | }
40 |
41 |
42 | public async process(ctx: IImageContext, params: string[]): Promise {
43 | const opt = this.validate(params);
44 | const metadata = await sharp(await ctx.image.toBuffer()).metadata(); // https://github.com/lovell/sharp/issues/2959
45 | if (!(metadata.width && metadata.height)) {
46 | throw new InvalidArgument('Can\'t read image\'s width and height');
47 | }
48 |
49 | const w = metadata.width;
50 | const h = metadata.height;
51 | const pages = metadata.pages ?? 1;
52 | const rects = Array.from({ length: pages }, (_, i) =>
53 | ``,
54 | );
55 | const mask = Buffer.from(``);
58 |
59 | ctx.image.composite([
60 | { input: mask, blend: 'dest-in' },
61 | ]);
62 | }
63 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/sharpen.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import * as is from '../../is';
4 | import { BaseImageAction } from './_base';
5 |
6 | export interface SharpenOpts extends IActionOpts {
7 | sharpen: number;
8 | }
9 |
10 | export class SharpenAction extends BaseImageAction {
11 | public readonly name: string = 'sharpen';
12 |
13 | public validate(params: string[]): ReadOnly {
14 | const opt: SharpenOpts = { sharpen: 0 };
15 |
16 | if (params.length !== 2) {
17 | throw new InvalidArgument('Sharpen param error, e.g: sharpen,100');
18 | }
19 | const s = Number.parseInt(params[1], 10);
20 | if (is.inRange(s, 50, 399)) {
21 | opt.sharpen = s;
22 | } else {
23 | throw new InvalidArgument('Sharpen be between 50 and 399');
24 | }
25 | return opt;
26 | }
27 |
28 |
29 | public async process(ctx: IImageContext, params: string[]): Promise {
30 | const opt = this.validate(params);
31 | ctx.image.sharpen(opt.sharpen / 100, 0.5, 1);
32 | }
33 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/strip-metadata.ts:
--------------------------------------------------------------------------------
1 | import { ReadOnly, IActionOpts, IProcessContext } from '..';
2 | import { BaseImageAction } from './_base';
3 |
4 | export class StripMetadataAction extends BaseImageAction {
5 | public readonly name: string = 'strip-metadata';
6 |
7 | validate(_: string[]): ReadOnly {
8 | return {};
9 | }
10 | process(_1: IProcessContext, _2: string[]): Promise {
11 | return Promise.resolve();
12 | }
13 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/image/threshold.ts:
--------------------------------------------------------------------------------
1 | import { IImageContext } from '.';
2 | import { IActionOpts, ReadOnly, InvalidArgument } from '..';
3 | import { BaseImageAction } from './_base';
4 |
5 |
6 | export interface ThresholdOpts extends IActionOpts {
7 | threshold: number;
8 | }
9 |
10 |
11 | export class ThresholdAction extends BaseImageAction {
12 | public readonly name: string = 'threshold';
13 |
14 | public beforeProcess(ctx: IImageContext, params: string[], _: number): void {
15 | const opts = this.validate(params);
16 |
17 | if (ctx.metadata.size && (ctx.metadata.size < opts.threshold)) {
18 | ctx.mask.disableAll();
19 | }
20 | }
21 |
22 | public validate(params: string[]): ReadOnly {
23 | if (params.length !== 2) {
24 | throw new InvalidArgument(`Invalid ${this.name} params, incomplete param`);
25 | }
26 | const t = Number.parseInt(params[1], 10);
27 | if (t <= 0) {
28 | throw new InvalidArgument(`Invalid ${this.name} params, threshold must be greater than zero`);
29 | }
30 | return {
31 | threshold: t,
32 | };
33 | }
34 |
35 | public async process(_1: IImageContext, _2: string[]): Promise {
36 | return Promise.resolve();
37 | }
38 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/index.ts:
--------------------------------------------------------------------------------
1 | import * as HttpErrors from 'http-errors';
2 | import { IBufferStore } from '../store';
3 |
4 | /**
5 | * A utility to make an object immutable.
6 | */
7 | export type ReadOnly = {
8 | readonly [K in keyof T]: ReadOnly;
9 | }
10 |
11 | export interface IActionMask {
12 | readonly length: number;
13 | getAction(index: number): string;
14 | isEnabled(index: number): boolean;
15 | isDisabled(index: number): boolean;
16 | enable(index: number): void;
17 | disable(index: number): void;
18 | disableAll(): void;
19 | filterEnabledActions(): string[];
20 | forEachAction(cb: (action: string, enabled: boolean, index: number) => void): void;
21 | }
22 |
23 | /**
24 | * Context object for processor.
25 | */
26 | export interface IProcessContext {
27 | /**
28 | * The context uri. e.g. 'a/b/example.jpg'
29 | */
30 | readonly uri: string;
31 |
32 | /**
33 | * The actions. e.g 'image/resize,w_100/format,png'.split('/')
34 | */
35 | readonly actions: string[];
36 |
37 | readonly mask: IActionMask;
38 |
39 | /**
40 | * A abstract store to get file data.
41 | * It can either get from s3 or local filesystem.
42 | */
43 | readonly bufferStore: IBufferStore;
44 |
45 | /**
46 | * Feature flags.
47 | */
48 | readonly features: { [key: string]: any };
49 |
50 | readonly headers: IHttpHeaders;
51 | }
52 |
53 | export interface IHttpHeaders {
54 | [key: string]: any;
55 | }
56 |
57 | export interface IProcessResponse {
58 | readonly data: any;
59 | readonly type: string;
60 | }
61 |
62 | /**
63 | * Processor interface.
64 | */
65 | export interface IProcessor {
66 |
67 | /**
68 | * The name of the processor.
69 | */
70 | readonly name: string;
71 |
72 | /**
73 | * Register action handlers for the processor.
74 | *
75 | * @param actions the action handlers
76 | */
77 | register(...actions: IAction[]): void;
78 |
79 | /**
80 | * Create a new context.
81 | * @param uri e.g. 'a/b/example.jpg'
82 | * @param actions e.g. 'image/resize,w_100/format,png'.split('/')
83 | * @param bufferStore
84 | */
85 | newContext(uri: string, actions: string[], bufferStore: IBufferStore): Promise;
86 |
87 | /**
88 | * Process each actions with a context.
89 | *
90 | * For example:
91 | *
92 | * ```ts
93 | * const bs = new SharpBufferStore(sharp({
94 | * create: {
95 | * width: 50,
96 | * height: 50,
97 | * channels: 3,
98 | * background: { r: 255, g: 0, b: 0 },
99 | * },
100 | * }).png());
101 | * const ctx = ImageProcessor.getInstance().newContext('example.jpg', 'image/resize,w_100,h_100,m_fixed,limit_0/'.split('/'));
102 | * await ImageProcessor.getInstance().process(ctx);
103 | * ```
104 | *
105 | * @param ctx the context
106 | */
107 | process(ctx: IProcessContext): Promise;
108 | }
109 |
110 | /**
111 | * An interface of action options.
112 | */
113 | export interface IActionOpts { }
114 |
115 | /**
116 | * An interface of action.
117 | */
118 | export interface IAction {
119 |
120 | /**
121 | * The name of the action.
122 | */
123 | readonly name: string;
124 |
125 | /**
126 | * Validate parameters and return an action option object.
127 | * Throw an error if it's invalid.
128 | *
129 | * For example:
130 | *
131 | * ```ts
132 | * action.validate('resize,m_mfit,h_100,w_100,,'.split(',');
133 | * ````
134 | *
135 | * @param params the parameters
136 | */
137 | validate(params: string[]): ReadOnly;
138 |
139 | /**
140 | * Process the action with the given parameters.
141 | *
142 | * For example:
143 | *
144 | * ```ts
145 | * action.process(ctx, 'resize,w_10,h_10'.split(','));
146 | * ```
147 | *
148 | * @param ctx the context
149 | * @param params the parameters
150 | */
151 | process(ctx: IProcessContext, params: string[]): Promise;
152 |
153 | /**
154 | * This function is called before processor new context.
155 | *
156 | * @param ctx the context
157 | * @param params the parameters
158 | */
159 | beforeNewContext(ctx: IProcessContext, params: string[], index: number): void;
160 |
161 | beforeProcess(ctx: IProcessContext, params: string[], index: number): void;
162 | }
163 |
164 | /**
165 | * Invalid argument error (HTTP 400).
166 | */
167 | export class InvalidArgument extends HttpErrors[400] { }
168 |
169 |
170 | export enum Features {
171 | AutoWebp = 'auto-webp',
172 | AutoOrient = 'auto-orient',
173 | ReturnInfo = 'return-info',
174 | ReadAllAnimatedFrames = 'read-all-animated-frames',
175 | LimitAnimatedFrames = 0,
176 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/processor/style.ts:
--------------------------------------------------------------------------------
1 | import { IAction, InvalidArgument, IProcessContext, IProcessor, IProcessResponse } from '.';
2 | import * as is from '../is';
3 | import { IBufferStore, IKVStore, MemKVStore } from '../store';
4 | import { ActionMask } from './image/_base';
5 | import { ImageProcessor } from './image/index';
6 | import { VideoProcessor } from './video';
7 |
8 |
9 | const PROCESSOR_MAP: { [key: string]: IProcessor } = {
10 | [ImageProcessor.getInstance().name]: ImageProcessor.getInstance(),
11 | [VideoProcessor.getInstance().name]: VideoProcessor.getInstance(),
12 | };
13 |
14 |
15 | export class StyleProcessor implements IProcessor {
16 | public static getInstance(kvstore?: IKVStore): StyleProcessor {
17 | if (!StyleProcessor._instance) {
18 | StyleProcessor._instance = new StyleProcessor();
19 | }
20 | if (kvstore) {
21 | StyleProcessor._instance._kvstore = kvstore;
22 | }
23 | return StyleProcessor._instance;
24 | }
25 | private static _instance: StyleProcessor;
26 |
27 | public readonly name: string = 'style';
28 | private _kvstore: IKVStore = new MemKVStore({});
29 |
30 | private constructor() { }
31 |
32 | public async newContext(uri: string, actions: string[], bufferStore: IBufferStore): Promise {
33 | return Promise.resolve({
34 | uri,
35 | actions,
36 | mask: new ActionMask(actions),
37 | bufferStore,
38 | headers: {},
39 | features: {},
40 | });
41 | }
42 |
43 | // e.g. https://Host/ObjectName?x-oss-process=style/
44 | public async process(ctx: IProcessContext): Promise {
45 | if (ctx.actions.length !== 2) {
46 | throw new InvalidArgument('Invalid style!');
47 | }
48 | const stylename = ctx.actions[1];
49 | if (!stylename.match(/^[\w\-_\.]{1,63}$/)) {
50 | throw new InvalidArgument('Invalid style name!');
51 | }
52 | // {
53 | // "id": "stylename",
54 | // "style": "image/resize,w_100,h_100"
55 | // }
56 | const { style } = await this._kvstore.get(stylename);
57 | const param = style; // e.g. image/resize,w_100,h_100,m_fixed,limit_0/
58 | if (is.string(param)) {
59 | const acts = param.split('/').filter((x: any) => x);
60 | const processor = PROCESSOR_MAP[acts[0]];
61 | if (!processor) {
62 | throw new InvalidArgument('Can Not find processor');
63 | }
64 | const context = await processor.newContext(ctx.uri, acts, ctx.bufferStore);
65 | return processor.process(context);
66 | } else {
67 | throw new InvalidArgument('Style not found');
68 | }
69 | }
70 |
71 | public register(..._: IAction[]): void { }
72 | }
73 |
--------------------------------------------------------------------------------
/source/new-image-handler/src/store.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as path from 'path';
3 | import * as DynamoDB from 'aws-sdk/clients/dynamodb';
4 | import * as S3 from 'aws-sdk/clients/s3';
5 | import * as sharp from 'sharp';
6 | import config from './config';
7 | import { IHttpHeaders } from './processor';
8 |
9 | /**
10 | * A abstract store to get file data.
11 | * It can either get from s3 or local filesystem.
12 | */
13 | export interface IStore {
14 |
15 | /**
16 | * Read all buffer from underlying.
17 | * Return both the buffer and the s3 object/file type.
18 | * Usually the file type is the file's suffix.
19 | *
20 | * @param p the path of the s3 object or the file
21 | * @param beforeGetFunc a hook function that will be executed before get
22 | */
23 | get(p: string, beforeGetFunc?: () => void): Promise;
24 |
25 | url(p: string): Promise;
26 | }
27 |
28 | export interface IKeyValue {
29 | [key: string]: any;
30 | }
31 |
32 | export interface IBufferStore extends IStore<{ buffer: Buffer; type: string; headers: IHttpHeaders }> { };
33 |
34 | export interface IKVStore extends IStore { }
35 |
36 | /**
37 | * A local file system based store.
38 | */
39 | export class LocalStore implements IBufferStore {
40 | public constructor(private root: string = '') { }
41 | public async get(p: string, _?: () => void):
42 | Promise<{ buffer: Buffer; type: string; headers: IHttpHeaders }> {
43 | p = path.join(this.root, p);
44 | return {
45 | buffer: await fs.promises.readFile(p),
46 | type: filetype(p),
47 | headers: {
48 | 'Etag': 'fake-etag',
49 | 'Last-Modified': 'Wed, 21 Oct 2014 07:28:00 GMT',
50 | 'Cache-Control': 'max-age',
51 | },
52 | };
53 | }
54 |
55 | public async url(p: string): Promise {
56 | return Promise.resolve(path.join(this.root, p));
57 | }
58 | }
59 |
60 | /**
61 | * S3 based store.
62 | */
63 | export class S3Store implements IBufferStore {
64 | private _s3: S3 = new S3({ region: config.region });
65 | public constructor(public readonly bucket: string) { }
66 | public async get(p: string, beforeGetFunc?: () => void):
67 | Promise<{ buffer: Buffer; type: string; headers: IHttpHeaders }> {
68 | beforeGetFunc?.();
69 | const res = await this._s3.getObject({
70 | Bucket: this.bucket,
71 | Key: p,
72 | }).promise();
73 |
74 | if (Buffer.isBuffer(res.Body)) {
75 | const headers: IHttpHeaders = {};
76 | if (res.ETag) { headers.Etag = res.ETag; }
77 | if (res.LastModified) { headers['Last-Modified'] = res.LastModified; }
78 | if (res.CacheControl) { headers['Cache-Control'] = res.CacheControl; }
79 | return {
80 | buffer: res.Body as Buffer,
81 | type: res.ContentType ?? '',
82 | headers,
83 | };
84 | };
85 | throw new Error('S3 response body is not a Buffer type');
86 | }
87 |
88 | public async url(p: string): Promise {
89 | return this._s3.getSignedUrlPromise('getObject', {
90 | Bucket: this.bucket,
91 | Key: p,
92 | Expires: 1200,
93 | });
94 | }
95 | }
96 |
97 | /**
98 | * A fake store. Only for unit test.
99 | */
100 | export class NullStore implements IBufferStore {
101 | public url(_: string): Promise {
102 | throw new Error('Method not implemented.');
103 | }
104 | public async get(p: string, _?: () => void): Promise<{ buffer: Buffer; type: string; headers: IHttpHeaders }> {
105 | return Promise.resolve({
106 | buffer: Buffer.from(p),
107 | type: '',
108 | headers: {},
109 | });
110 | }
111 | }
112 |
113 | /**
114 | * A sharp image store. Only for unit test.
115 | */
116 | export class SharpBufferStore implements IBufferStore {
117 | constructor(private image: sharp.Sharp) { }
118 | public url(_: string): Promise {
119 | throw new Error('Method not implemented.');
120 | }
121 |
122 | async get(_: string, __?: () => void): Promise<{ buffer: Buffer; type: string; headers: IHttpHeaders }> {
123 | const { data, info } = await this.image.toBuffer({ resolveWithObject: true });
124 | return { buffer: data, type: info.format, headers: {} };
125 | }
126 | }
127 |
128 |
129 | export class DynamoDBStore implements IKVStore {
130 | private _ddb = new DynamoDB.DocumentClient({ region: config.region });
131 | public constructor(public readonly tableName: string) { }
132 | public url(_: string): Promise {
133 | throw new Error('Method not implemented.');
134 | }
135 | public async get(key: string, _?: () => void): Promise {
136 | const data = await this._ddb.get({
137 | TableName: this.tableName,
138 | Key: { id: key },
139 | }).promise();
140 | return data.Item ?? {};
141 | }
142 | }
143 |
144 | export class MemKVStore implements IKVStore {
145 | public constructor(public readonly dict: IKeyValue) { }
146 | public url(_: string): Promise {
147 | throw new Error('Method not implemented.');
148 | }
149 |
150 | public async get(key: string, _?: () => void): Promise {
151 | return Promise.resolve(this.dict[key] ?? {});
152 | }
153 | }
154 |
155 |
156 | function filetype(file: string) {
157 | return path.extname(file).substring(1);
158 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/style.json:
--------------------------------------------------------------------------------
1 | {
2 | "box100": {
3 | "style": "image/resize,w_100,h_100,m_fixed,limit_0/"
4 | }
5 | }
--------------------------------------------------------------------------------
/source/new-image-handler/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module "style.json" {
2 | const value: { [key: string]: { [k: string]: string } };
3 | export default value;
4 | }
5 |
6 | interface CacheObject {
7 | body: Buffer;
8 | }
--------------------------------------------------------------------------------
/source/new-image-handler/test/bench/README.md:
--------------------------------------------------------------------------------
1 | # Benchmark test
2 |
3 | ## curl.sh: make http requests to urls in parallel
4 |
5 | ```
6 | # edit urls.txt
7 |
8 | $ ./curl.sh urls.txt
9 | $ N=10 ./curl.sh urls.txt # make 10 http requests simultaneously
10 | ```
11 |
12 | ## vegeta: http load test tools
13 |
14 | install https://github.com/tsenart/vegeta firstly
15 |
16 | ```
17 | # edit vegeta-urls.txt
18 | cat vegeta-urls.txt | vegeta attack -duration=1m -rate=300 -timeout=300s -format=http | vegeta report
19 | ```
--------------------------------------------------------------------------------
/source/new-image-handler/test/bench/curl.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # ./curl.sh urls.txt 2
3 |
4 | URLS=$1
5 | N=${2:-2}
6 |
7 | echo "request ${N} urls in parallel"
8 | printf "time_total(sec)\thttp_code\tsize_download(bytes)\turl\n"
9 | grep -v '^#' ${URLS} | xargs -P ${N} -I {} curl -s "{}" -o /dev/null -w "%{time_total}\t%{http_code}\t%{size_download}\t{}\n"
--------------------------------------------------------------------------------
/source/new-image-handler/test/bench/perf.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'os';
2 | import * as Benchmark from 'benchmark';
3 | import * as sharp from 'sharp';
4 | import { convert } from '../../src/imagemagick';
5 | import { fixtureStore } from '../processor/image/utils';
6 |
7 | sharp.simd(true);
8 | sharp.cache(false);
9 | sharp.concurrency(os.cpus().length);
10 |
11 | const suite = new Benchmark.Suite('sharp vs imagemagick');
12 |
13 | suite
14 | .add('sharp', {
15 | defer: true,
16 | fn: async (deferred: any) => {
17 | const image = sharp((await fixtureStore.get('example.jpg')).buffer);
18 | image
19 | .resize(200, 200, { fit: 'inside' })
20 | .blur(3)
21 | .jpeg({ quality: 80 });
22 |
23 | await image.toBuffer();
24 |
25 | deferred.resolve();
26 | },
27 | })
28 | .add('imagemagick', {
29 | defer: true,
30 | fn: async (deferred: any) => {
31 | const buffer = (await fixtureStore.get('example.jpg')).buffer;
32 |
33 | await convert(buffer, [
34 | '-resize', '200x200',
35 | '-blur', '3x3',
36 | '-quality', '80',
37 | ]);
38 |
39 | deferred.resolve();
40 | },
41 | })
42 | .on('complete', () => {
43 | suite.each((each: Benchmark) => {
44 | console.log(each.name,
45 | precision(each.stats.mean * 1000) + ' ms',
46 | precision(each.hz) + ' ops/sec',
47 | each.stats.sample.length + ' samples');
48 | });
49 | console.log('Fastest is ' + suite.filter('fastest').map('name'));
50 | })
51 | .run({ async: true });
52 |
53 |
54 | function precision(v: number) {
55 | return Math.round(v * 100) / 100;
56 | }
--------------------------------------------------------------------------------
/source/new-image-handler/test/bench/urls.txt:
--------------------------------------------------------------------------------
1 | # Each line must be a URL.
2 | # Comments for lines starting with #.
3 | # Example:
4 | # https:///example.jpg?x-oss-process=image/format,gif/quality,q_80
--------------------------------------------------------------------------------
/source/new-image-handler/test/bench/vegeta-urls.txt:
--------------------------------------------------------------------------------
1 | GET http:///example.jpg?x-oss-process=image/resize,s_300/quality,q_80/auto-orient,0/interlace,1/format,jpg
--------------------------------------------------------------------------------
/source/new-image-handler/test/default.test.ts:
--------------------------------------------------------------------------------
1 | import { parseRequest, kvstore } from '../src/default';
2 |
3 | test('parseActions empty', () => {
4 | expect(parseRequest('', {})).toEqual({
5 | uri: '',
6 | actions: [],
7 | });
8 | expect(parseRequest('/example.jpg', {})).toEqual({
9 | uri: 'example.jpg',
10 | actions: [],
11 | });
12 | });
13 |
14 | test('parseActions x-oss-process', () => {
15 | expect(parseRequest('', {
16 | 'x-oss-process': '',
17 | })).toEqual({
18 | uri: '',
19 | actions: [],
20 | });
21 | expect(parseRequest('', {
22 | 'x-oss-process': '//image/resize,w_100,h_100,m_fixed,limit_0//quality,q_1//',
23 | })).toEqual({
24 | uri: '',
25 | actions: ['image', 'resize,w_100,h_100,m_fixed,limit_0', 'quality,q_1'],
26 | });
27 | });
28 |
29 | test('parseActions custom delimiter', () => {
30 | expect(parseRequest('/example.jpg@!ABCabc.-_', {})).toEqual({
31 | uri: 'example.jpg',
32 | actions: ['style', 'ABCabc.-_'],
33 | });
34 | expect(parseRequest('/example.jpg!ABCabc.-_', {})).toEqual({
35 | uri: 'example.jpg',
36 | actions: ['style', 'ABCabc.-_'],
37 | });
38 | expect(() => parseRequest('/example.jpg@! ', {})).toThrowError(/Empty style name/);
39 | });
40 |
41 | test('kvstore', async () => {
42 | const store = kvstore();
43 | expect(await store.get('box100')).toEqual({
44 | style: 'image/resize,w_100,h_100,m_fixed,limit_0/',
45 | });
46 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/e2e/README.md:
--------------------------------------------------------------------------------
1 | # End 2 end test
2 |
3 | ## mkhtml.py is a side by side image comparison tools
4 |
5 | ```
6 | $ chmod +x mkhtml.py
7 | $ B=http://HOST/ ./mkhtml.py
8 | html has been created at index.html
9 |
10 | # then open the index.html in browser
11 | ```
--------------------------------------------------------------------------------
/source/new-image-handler/test/e2e/mkhtml.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import os
3 |
4 | TEMPLATE = r'''
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | NO. |
13 | A |
14 | B |
15 |
16 |
17 |
18 | {body}
19 |
20 |
21 |
22 |
23 |
24 | '''
25 | PATHS = [
26 | # resize
27 | 'example.jpg?x-oss-process=image/resize,w_100',
28 | 'example.jpg?x-oss-process=image/resize,h_50',
29 | 'example.jpg?x-oss-process=image/resize,w_100,m_lfit',
30 | 'example.jpg?x-oss-process=image/resize,w_100,m_mfit',
31 | 'example.jpg?x-oss-process=image/resize,w_100,h_67,m_fill',
32 | 'example.jpg?x-oss-process=image/resize,w_100,m_pad',
33 | 'example.jpg?x-oss-process=image/resize,h_100,w_100,m_fixed',
34 | 'example.jpg?x-oss-process=image/resize,h_100,m_lfit',
35 | 'example.jpg?x-oss-process=image/resize,l_100',
36 | 'example.jpg?x-oss-process=image/resize,m_fixed,h_100,w_100',
37 | 'example.jpg?x-oss-process=image/resize,m_fill,h_100,w_100',
38 | 'example.jpg?x-oss-process=image/resize,m_pad,h_100,w_100,color_FF0000',
39 | 'example.jpg?x-oss-process=image/resize,p_50',
40 | # circle
41 | 'example.jpg?x-oss-process=image/circle,r_100',
42 | # crop
43 | 'example.jpg?x-oss-process=image/crop,x_100,y_50',
44 | 'example.jpg?x-oss-process=image/crop,x_100,y_50,w_100,h_100',
45 | 'example.jpg?x-oss-process=image/crop,x_10,y_10,w_200,h_200,g_se',
46 | # indexcrop
47 | 'example.jpg?x-oss-process=image/indexcrop,x_100,i_0',
48 | # rounded-corners
49 | 'example.jpg?x-oss-process=image/rounded-corners,r_30',
50 | 'example.jpg?x-oss-process=image/crop,w_100,h_100/rounded-corners,r_10/format,png',
51 | # rotate
52 | 'example.jpg?x-oss-process=image/rotate,70',
53 | # blur
54 | 'example.jpg?x-oss-process=image/blur,r_3,s_2',
55 | # bright
56 | 'example.jpg?x-oss-process=image/bright,50',
57 | 'example.jpg?x-oss-process=image/bright,-50',
58 | # sharpen
59 | 'example.jpg?x-oss-process=image/sharpen,100',
60 | # contrast
61 | 'example.jpg?x-oss-process=image/contrast,-50',
62 | 'example.jpg?x-oss-process=image/contrast,50',
63 | # quality
64 | 'example.jpg?x-oss-process=image/resize,w_100/quality,q_30',
65 | 'example.jpg?x-oss-process=image/resize,w_100/quality,Q_30',
66 | # interlace
67 | 'example.jpg?x-oss-process=image/resize,w_200/interlace,1',
68 | # watermark
69 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ',
70 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_nw',
71 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_north',
72 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_ne',
73 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_west',
74 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_center',
75 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_east',
76 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_sw',
77 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_south',
78 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,g_se',
79 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_100,color_FFFFFF,shadow_0',
80 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_100,color_FFFFFF,shadow_50',
81 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_100,color_FFFFFF,shadow_100',
82 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_30',
83 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_500',
84 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,size_1000',
85 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,rotate_45',
86 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,rotate_90',
87 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,rotate_180',
88 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,fill_1',
89 | 'example.jpg?x-oss-process=image/watermark,text_SGVsbG8gV29ybGQ,fill_0',
90 | 'example.jpg?x-oss-process=image/quality,q_70/watermark,text_4paI4paI4paI4paI,g_se,x_0,y_0,size_24,shadow_0,color_3E3E3E/watermark,text_5Yqo5Zu-,g_se,x_6,y_4,size_24,shadow_0,color_FFFFFF/resize,w_490',
91 | 'example.jpg?x-oss-process=image/watermark,text_SG9Zb0xBQkBXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1cyNA==,size_26,color_FFFFFF,shadow_50,t_70,g_se,x_37,y_77',
92 | ]
93 | EP_A = os.environ.get('A', 'https://image-demo-oss-zhangjiakou.oss-cn-zhangjiakou.aliyuncs.com/')
94 | EP_B = os.environ.get('B')
95 |
96 |
97 | def row(i, a, b):
98 | return '\n'.join([
99 | '',
100 | f' {i} | ',
101 | f'  | ',
102 | f'  | ',
103 | '
',
104 | ])
105 |
106 |
107 | s = TEMPLATE.format(body='\n'.join([
108 | row(i, os.path.join(EP_A, p), os.path.join(EP_B, p)) for i, p in enumerate(PATHS)
109 | ]))
110 | fname = 'index.html'
111 |
112 | with open(fname, 'w') as fp:
113 | fp.write(s)
114 |
115 | print(f'html has been created at {fname}')
116 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/.gitignore:
--------------------------------------------------------------------------------
1 | *.gif
2 | *.png
3 | *.PNG
4 | *.jpeg
5 | *.jpg
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/aws_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/aws_logo.png
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/example.gif
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/example.jpg
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/example.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/example.mp4
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/f.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/f.jpg
--------------------------------------------------------------------------------
/source/new-image-handler/test/fixtures/gray-200x100.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wchaws/serverless-image-handler/423c5e761af468ba2fa8eb9fbbd700573c614c0a/source/new-image-handler/test/fixtures/gray-200x100.jpg
--------------------------------------------------------------------------------
/source/new-image-handler/test/imagemagick.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { convert, identify } from '../src/imagemagick';
3 | import { fixtureStore } from './processor/image/utils';
4 |
5 | test.skip('imagemagick convert', async () => {
6 | const { buffer } = await fixtureStore.get('example.jpg');
7 | const bufout = await convert(buffer, ['-resize', '10%']);
8 | const metadata = await sharp(bufout).metadata();
9 |
10 | expect(metadata.width).toBe(40);
11 | expect(metadata.height).toBe(27);
12 | });
13 |
14 | test.skip('imagemagick identify', async () => {
15 | const { buffer } = await fixtureStore.get('example.jpg');
16 | const bufout = await identify(buffer, ['-format', '%Q']);
17 |
18 | expect(bufout.toString()).toBe('82');
19 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/index-lambda.test.ts:
--------------------------------------------------------------------------------
1 | import { URL, URLSearchParams } from 'url';
2 | import * as sharp from 'sharp';
3 | import { handler } from '../src/index-lambda';
4 |
5 | function URLSearchParams2Obj(param: URLSearchParams) {
6 | const o: { [k: string]: string } = {};
7 | for (const [key, value] of param.entries()) {
8 | o[key] = value;
9 | }
10 | return o;
11 | }
12 |
13 | function mkevt(p: string) {
14 | const u = new URL(p, 'http://test');
15 | return {
16 | version: '2.0',
17 | routeKey: 'ANY /{proxy+}',
18 | rawPath: u.pathname,
19 | rawQueryString: u.search.substring(1),
20 | cookies: [
21 | 's_fid=********',
22 | ],
23 | headers: {
24 | 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9',
25 | 'accept-encoding': 'gzip, deflate, br',
26 | 'accept-language': 'en,zh-CN;q=0.9,zh;q=0.8',
27 | 'content-length': '0',
28 | 'host': '********.execute-api.us-west-2.amazonaws.com',
29 | 'sec-ch-ua': '" Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"',
30 | 'sec-ch-ua-mobile': '?0',
31 | 'sec-ch-ua-platform': '"macOS"',
32 | 'sec-fetch-dest': 'document',
33 | 'sec-fetch-mode': 'navigate',
34 | 'sec-fetch-site': 'none',
35 | 'sec-fetch-user': '?1',
36 | 'upgrade-insecure-requests': '1',
37 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
38 | 'x-amzn-trace-id': 'Root=1-625b948f-514240be75d2c50924dcaf42',
39 | 'x-forwarded-for': '127.0.0.1',
40 | 'x-forwarded-port': '443',
41 | 'x-forwarded-proto': 'https',
42 | },
43 | queryStringParameters: URLSearchParams2Obj(u.searchParams),
44 | requestContext: {
45 | accountId: '********',
46 | apiId: '********',
47 | domainName: '********.execute-api.us-west-2.amazonaws.com',
48 | domainPrefix: '********',
49 | http: {
50 | method: 'GET',
51 | path: u.pathname,
52 | protocol: 'HTTP/1.1',
53 | sourceIp: '127.0.0.1',
54 | userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36',
55 | },
56 | requestId: '*****',
57 | routeKey: 'ANY /{proxy+}',
58 | stage: '$default',
59 | time: '17/Apr/2022:04:16:15 +0000',
60 | timeEpoch: 1650168975606,
61 | },
62 | pathParameters: {
63 | proxy: 'example.jpg',
64 | },
65 | isBase64Encoded: false,
66 | };
67 | }
68 |
69 |
70 | test('index-lambda.ts example.jpg?x-oss-process=image/resize,w_100/quality,q_50', async () => {
71 | const res: any = await handler(mkevt('example.jpg?x-oss-process=image/resize,w_100/quality,q_50'));
72 |
73 | expect(res.isBase64Encoded).toBeTruthy();
74 | expect(res.statusCode).toBe(200);
75 | expect(res.headers['Content-Type']).toBe('image/jpeg');
76 | expect(res.headers['Last-Modified']).toBe('Wed, 21 Oct 2014 07:28:00 GMT');
77 | expect(res.headers['Cache-Control']).toBe('max-age');
78 |
79 | const metadata = await sharp(Buffer.from(res.body, 'base64')).metadata();
80 |
81 | expect(metadata.width).toBe(100);
82 | expect(metadata.height).toBe(67);
83 | expect(metadata.size).toBe(1352);
84 | expect(metadata.format).toBe('jpeg');
85 | });
86 |
87 | test('index-lambda.ts example.gif?x-oss-process=image/resize,w_100/quality,q_50', async () => {
88 | const res: any = await handler(mkevt('example.gif?x-oss-process=image/resize,w_100/quality,q_50'));
89 |
90 | expect(res.isBase64Encoded).toBeTruthy();
91 | expect(res.statusCode).toBe(200);
92 | expect(res.headers['Content-Type']).toBe('image/gif');
93 |
94 | const metadata = await sharp(Buffer.from(res.body, 'base64')).metadata();
95 |
96 | expect(metadata.width).toBe(100);
97 | expect(metadata.height).toBe(60);
98 | expect(metadata.size).toBe(3544);
99 | expect(metadata.format).toBe('gif');
100 | expect(metadata.pages).toBe(3);
101 | });
102 |
103 | test('index-lambda.ts example.gif?x-oss-process=image/format,png', async () => {
104 | const res: any = await handler(mkevt('example.gif?x-oss-process=image/format,png'));
105 |
106 | expect(res.isBase64Encoded).toBeTruthy();
107 | expect(res.statusCode).toBe(200);
108 | expect(res.headers['Content-Type']).toBe('image/png');
109 |
110 | const metadata = await sharp(Buffer.from(res.body, 'base64')).metadata();
111 |
112 | expect(metadata.width).toBe(500);
113 | expect(metadata.height).toBe(300);
114 | expect(metadata.format).toBe('png');
115 | });
116 |
117 | test('index-lambda.ts example.gif?x-oss-process=image/resize,w_1/info', async () => {
118 | const res: any = await handler(mkevt('example.gif?x-oss-process=image/resize,w_1/info'));
119 |
120 | expect(res.isBase64Encoded).toBeFalsy();
121 | expect(res.statusCode).toBe(200);
122 | expect(res.headers['Content-Type']).toBe('application/json');
123 | expect(res.body).toBe(JSON.stringify({
124 | FileSize: {
125 | value: '21957',
126 | },
127 | Format: {
128 | value: 'gif',
129 | },
130 | ImageHeight: {
131 | value: '300',
132 | },
133 | ImageWidth: {
134 | value: '500',
135 | },
136 | }));
137 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/_base.test.ts:
--------------------------------------------------------------------------------
1 | import { ActionMask, split1 } from '../../../src/processor/image/_base';
2 |
3 | test(`${ActionMask.name} all enabled by default`, () => {
4 | const actions = '1 1 1 1 1 1'.split(' ');
5 | const s = new ActionMask(actions);
6 |
7 | expect(s.length).toBe(actions.length);
8 | expect(s.filterEnabledActions()).toEqual(actions);
9 | });
10 |
11 | test(`${ActionMask.name} getAction`, () => {
12 | const actions = '1 2 3 4 5 6'.split(' ');
13 | const s = new ActionMask(actions);
14 |
15 | expect(s.length).toBe(actions.length);
16 |
17 | s.disable(0);
18 | s.enable(0);
19 | expect(s.isDisabled(0)).toBeFalsy();
20 |
21 | s.forEachAction((action, enable, index) => {
22 | expect(s.getAction(index)).toEqual(action);
23 | expect(s.isEnabled(index)).toBe(enable);
24 | expect(action).toBe(`${1 + index}`);
25 | });
26 | });
27 |
28 | test(`${ActionMask.name} enable/disable`, () => {
29 | const actions = '1 2 3 4 5 6'.split(' ');
30 | const s = new ActionMask(actions);
31 |
32 | expect(s.length).toBe(actions.length);
33 | s.disable(0);
34 | s.disable(1);
35 | s.disable(2);
36 | expect(s.filterEnabledActions().length).toBe(3);
37 | expect(s.filterEnabledActions()).toEqual('4 5 6'.split(' '));
38 | });
39 |
40 | test(`${ActionMask.name} forEach enable/disable`, () => {
41 | const actions = '1 2 3 4 5 6'.split(' ');
42 | const s = new ActionMask(actions);
43 |
44 | expect(s.length).toBe(actions.length);
45 | s.forEachAction((_, enabled, index) => {
46 | expect(enabled).toBeTruthy();
47 | s.disable(index);
48 | });
49 | expect(s.filterEnabledActions()).toEqual([]);
50 | });
51 |
52 | test(`${ActionMask.name} index out of range`, () => {
53 | const actions = '1 2 3 4 5 6'.split(' ');
54 | const s = new ActionMask(actions);
55 |
56 | expect(() => {
57 | s.enable(-1);
58 | }).toThrowError(/Index out of range/);
59 | });
60 |
61 | test(`${split1.name} split`, () => {
62 | const s = 'text_SG9ZT0xBQkBvZmZjaWFsK0DvvIEjQO-8gSPvvIEj77-l77yBQO-_pe-8gUAj77-l77yB77-l77yBZQ==';
63 | expect(split1(s, '_')).toEqual([
64 | 'text',
65 | 'SG9ZT0xBQkBvZmZjaWFsK0DvvIEjQO-8gSPvvIEj77-l77yBQO-_pe-8gUAj77-l77yB77-l77yBZQ==',
66 | ]);
67 | });
68 |
69 |
70 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/auto-orient.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { AutoOrientAction } from '../../../src/processor/image/auto-orient';
3 | import { mkctx } from './utils';
4 |
5 | test('auto-orient action validate', () => {
6 | const action = new AutoOrientAction();
7 | const param0 = action.validate('auto-orient,0'.split(','));
8 | expect(param0).toEqual({
9 | auto: false,
10 | });
11 | const param1 = action.validate('auto-orient,1'.split(','));
12 | expect(param1).toEqual({
13 | auto: true,
14 | });
15 |
16 |
17 | expect(() => {
18 | action.validate('auto-orient,1,2'.split(','));
19 | }).toThrowError(/Auto-orient param error, e.g: auto-orient,1/);
20 |
21 | expect(() => {
22 | action.validate('auto-orient'.split(','));
23 | }).toThrowError(/Auto-orient param error, e.g: auto-orient,1/);
24 |
25 |
26 | expect(() => {
27 | action.validate('auto-orient,xx'.split(','));
28 | }).toThrowError(/Auto-orient param must be 0 or 1/);
29 |
30 | expect(() => {
31 | action.validate('auto-orient,20'.split(','));
32 | }).toThrowError(/Auto-orient param must be 0 or 1/);
33 |
34 | });
35 |
36 |
37 | test('quality action', async () => {
38 | const ctx = await mkctx('example.jpg');
39 | const action = new AutoOrientAction();
40 | await action.process(ctx, 'auto-orient,1'.split(','));
41 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
42 |
43 | expect(info.format).toBe(sharp.format.jpeg.id);
44 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/blur.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { BlurAction } from '../../../src/processor/image/blur';
3 | import { mkctx } from './utils';
4 |
5 | test('quality action validate', () => {
6 | const action = new BlurAction();
7 | const param1 = action.validate('blur,r_3,s_2'.split(','));
8 |
9 | expect(param1).toEqual({
10 | r: 3,
11 | s: 2,
12 | });
13 | expect(() => {
14 | action.validate('blur'.split(','));
15 | }).toThrowError(/blur param error, e.g: blur,r_3,s_2/);
16 | expect(() => {
17 | action.validate('blur,xx'.split(','));
18 | }).toThrowError(/Unkown param/);
19 | expect(() => {
20 | action.validate('blur,r_-1'.split(','));
21 | }).toThrowError(/Blur param 'r' must be between 0 and 50/);
22 | expect(() => {
23 | action.validate('blur,s_51'.split(','));
24 | }).toThrowError(/Blur param 's' must be between 0 and 50/);
25 | expect(() => {
26 | action.validate('blur,s_1111'.split(','));
27 | }).toThrowError(/Blur param 's' must be between 0 and 50/);
28 | });
29 |
30 |
31 | test('quality action', async () => {
32 | const ctx = await mkctx('example.jpg');
33 | const action = new BlurAction();
34 | await action.process(ctx, 'blur,r_5,s_2'.split(','));
35 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
36 |
37 | expect(info.format).toBe(sharp.format.jpeg.id);
38 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/bright.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { BrightAction } from '../../../src/processor/image/bright';
3 | import { mkctx } from './utils';
4 |
5 | test('bright action validate', () => {
6 | const action = new BrightAction();
7 | const param1 = action.validate('bright,50'.split(','));
8 | expect(param1).toEqual({
9 | bright: 50,
10 | });
11 | expect(() => {
12 | action.validate('bright'.split(','));
13 | }).toThrowError(/Bright param error, e.g: bright,50/);
14 |
15 | expect(() => {
16 | action.validate('bright,23,32'.split(','));
17 | }).toThrowError(/Bright param error, e.g: bright,50/);
18 |
19 | expect(() => {
20 | action.validate('bright,xx'.split(','));
21 | }).toThrowError(/Bright must be between -100 and 100/);
22 |
23 | expect(() => {
24 | action.validate('bright,-101'.split(','));
25 | }).toThrowError(/Bright must be between -100 and 100/);
26 |
27 | expect(() => {
28 | action.validate('bright,101'.split(','));
29 | }).toThrowError(/Bright must be between -100 and 100/);
30 |
31 | });
32 |
33 |
34 | test('bright action', async () => {
35 | const ctx = await mkctx('example.jpg');
36 | const action = new BrightAction();
37 | await action.process(ctx, 'bright,50'.split(','));
38 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
39 |
40 | expect(info.format).toBe(sharp.format.jpeg.id);
41 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/cgif.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { CgifAction } from '../../../src/processor/image/cgif';
3 | import { mkctx } from './utils';
4 |
5 |
6 | test('cgif,s_5', async () => {
7 | const ctx = await mkctx('example.gif');
8 | const action = new CgifAction();
9 | await action.beforeNewContext(ctx, 'cgif,s_5'.split(','));
10 | const { info } = await ctx.image.gif().toBuffer({ resolveWithObject: true });
11 | expect(info.format).toBe(sharp.format.gif.id);
12 | });
13 |
14 | test('cgif beforeNewContext', async () => {
15 | const ctx = await mkctx('example.gif');
16 | const action = new CgifAction();
17 |
18 | expect(() => {
19 | action.beforeNewContext(ctx, 'cgif,s'.split(','));
20 | }).toThrowError(/Unkown param: \"s\"/);
21 |
22 | expect(() => {
23 | action.beforeNewContext(ctx, 'cgif,s_d'.split(','));
24 | }).toThrowError(/Unkown param: \"s\"/);
25 |
26 | expect(() => {
27 | action.beforeNewContext(ctx, 'cgif'.split(','));
28 | }).toThrowError(/Cut gif param error, e.g: cgif,s_1/);
29 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/circle.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { CircleAction } from '../../../src/processor/image/circle';
3 | import { mkctx } from './utils';
4 |
5 | test('circle,r_500', async () => {
6 | const ctx = await mkctx('example.jpg');
7 |
8 | const action = new CircleAction();
9 | await action.process(ctx, 'circle,r_500'.split(','));
10 |
11 | const { info } = await ctx.image.png().toBuffer({ resolveWithObject: true });
12 |
13 | expect(info.channels).toBe(4);
14 | expect(info.format).toBe(sharp.format.png.id);
15 | });
16 |
17 | test('circle,r_100', async () => {
18 | const ctx = await mkctx('example.jpg');
19 |
20 | const action = new CircleAction();
21 | await action.process(ctx, 'circle,r_100'.split(','));
22 |
23 | const { info } = await ctx.image.png().toBuffer({ resolveWithObject: true });
24 |
25 | expect(info.channels).toBe(4);
26 | expect(info.format).toBe(sharp.format.png.id);
27 | expect(info.width).toBe(201);
28 | expect(info.height).toBe(201);
29 | });
30 |
31 | test('quality action validate', () => {
32 | const action = new CircleAction();
33 | const param1 = action.validate('circle,r_30'.split(','));
34 |
35 | expect(param1).toEqual({ r: 30 });
36 | expect(() => {
37 | action.validate('circle,r_'.split(','));
38 | }).toThrowError(/must be between 1 and 4096/);
39 | expect(() => {
40 | action.validate('blur,xx'.split(','));
41 | }).toThrowError(/Unkown param/);
42 | });
43 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/contrast.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { ContrastAction } from '../../../src/processor/image/contrast';
3 | import { mkctx } from './utils';
4 |
5 | test('quality action validate', () => {
6 | const action = new ContrastAction();
7 | const param1 = action.validate('contrast,-50'.split(','));
8 | expect(param1).toEqual({
9 | contrast: -50,
10 | });
11 |
12 | expect(() => {
13 | action.validate('contrast'.split(','));
14 | }).toThrowError(/Contrast param error, e.g: contrast,-50/);
15 |
16 | expect(() => {
17 | action.validate('contrast,xx,22'.split(','));
18 | }).toThrowError(/Contrast param error, e.g: contrast,-50/);
19 |
20 | expect(() => {
21 | action.validate('contrast,abc'.split(','));
22 | }).toThrowError(/Contrast must be between -100 and 100/);
23 |
24 |
25 | expect(() => {
26 | action.validate('contrast,101'.split(','));
27 | }).toThrowError(/Contrast must be between -100 and 100/);
28 |
29 | expect(() => {
30 | action.validate('contrast,-101'.split(','));
31 | }).toThrowError(/Contrast must be between -100 and 100/);
32 |
33 |
34 | });
35 |
36 |
37 | test('quality action', async () => {
38 | const ctx = await mkctx('example.jpg');
39 | const action = new ContrastAction();
40 | await action.process(ctx, 'contrast,-50'.split(','));
41 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
42 |
43 | expect(info.format).toBe(sharp.format.jpeg.id);
44 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/format.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { Features } from '../../../src/processor';
3 | import { FormatAction } from '../../../src/processor/image/format';
4 | import { mkctx } from './utils';
5 |
6 | test('format action validate', () => {
7 | const action = new FormatAction();
8 | const param1 = action.validate('format,jpg'.split(','));
9 | expect(param1).toEqual({
10 | format: 'jpg',
11 | });
12 |
13 | expect(() => {
14 | action.validate('format'.split(','));
15 | }).toThrowError(/Format param error, e.g: format,jpg/);
16 |
17 |
18 | expect(() => {
19 | action.validate('format,jpg,png'.split(','));
20 | }).toThrowError(/Format param error, e.g: format,jpg/);
21 |
22 | expect(() => {
23 | action.validate('format,abc'.split(','));
24 | }).toThrowError(/Format must be one of/);
25 |
26 |
27 | expect(() => {
28 | action.validate('format,12'.split(','));
29 | }).toThrowError(/Format must be one of/);
30 |
31 | });
32 |
33 |
34 | test('format action', async () => {
35 | const ctx = await mkctx('example.jpg');
36 | const action = new FormatAction();
37 | await action.process(ctx, 'format,png'.split(','));
38 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
39 | expect(info.format).toBe(sharp.format.png.id);
40 | });
41 |
42 | test('format action', async () => {
43 | const ctx = await mkctx('example.jpg');
44 | const action = new FormatAction();
45 | await action.process(ctx, 'format,webp'.split(','));
46 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
47 | expect(info.format).toBe(sharp.format.webp.id);
48 | });
49 |
50 | test('format action', async () => {
51 | const ctx = await mkctx('example.jpg');
52 | const action = new FormatAction();
53 | await action.process(ctx, 'format,jpg'.split(','));
54 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
55 | expect(info.format).toBe(sharp.format.jpeg.id);
56 | });
57 |
58 | test('format,jpeg', async () => {
59 | const ctx = await mkctx('example.jpg');
60 | const action = new FormatAction();
61 | await action.process(ctx, 'format,jpeg'.split(','));
62 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
63 | expect(info.format).toBe(sharp.format.jpeg.id);
64 | });
65 |
66 | test('format,gif', async () => {
67 | const ctx = await mkctx('example.gif');
68 | const action = new FormatAction();
69 | await action.process(ctx, 'format,gif'.split(','));
70 | const { info, data } = await ctx.image.toBuffer({ resolveWithObject: true });
71 |
72 | expect(info.format).toBe(sharp.format.gif.id);
73 |
74 | const metadata = await sharp(data, { animated: true }).metadata();
75 | expect(metadata.pages).toBe(3);
76 | });
77 |
78 | test(`format,png disable ${Features.ReadAllAnimatedFrames}`, async () => {
79 | const ctx = await mkctx('example.gif');
80 | const action = new FormatAction();
81 | action.beforeNewContext(ctx, 'format,png'.split(','));
82 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(false);
83 | action.beforeNewContext(ctx, 'format,jpg'.split(','));
84 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(false);
85 | action.beforeNewContext(ctx, 'format,jpeg'.split(','));
86 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(false);
87 | });
88 |
89 |
90 | test(`format,png enable ${Features.ReadAllAnimatedFrames}`, async () => {
91 | const ctx = await mkctx('example.gif');
92 | const action = new FormatAction();
93 | action.beforeNewContext(ctx, 'format,gif'.split(','));
94 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(true);
95 | action.beforeNewContext(ctx, 'format,webp'.split(','));
96 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(true);
97 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/grey.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { GreyAction } from '../../../src/processor/image/grey';
3 | import { mkctx } from './utils';
4 |
5 | test('quality action validate', () => {
6 | const action = new GreyAction();
7 | const param1 = action.validate('grey,1'.split(','));
8 | expect(param1).toEqual({
9 | grey: true,
10 | });
11 |
12 | expect(() => {
13 | action.validate('grey'.split(','));
14 | }).toThrowError(/Grey param error, e.g: grey,1/);
15 |
16 | expect(() => {
17 | action.validate('grey,xx,22'.split(','));
18 | }).toThrowError(/Grey param error, e.g: grey,1/);
19 |
20 | expect(() => {
21 | action.validate('grey,ab'.split(','));
22 | }).toThrowError(/Grey must be 0 or 1/);
23 |
24 | expect(() => {
25 | action.validate('grey,-1'.split(','));
26 | }).toThrowError(/Grey must be 0 or 1/);
27 |
28 |
29 | });
30 |
31 | test('quality action', async () => {
32 | const ctx = await mkctx('example.jpg');
33 | const action = new GreyAction();
34 | await action.process(ctx, 'grey,1'.split(','));
35 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
36 |
37 | expect(info.format).toBe(sharp.format.jpeg.id);
38 | });
39 |
40 |
41 | test('quality action', async () => {
42 | const ctx = await mkctx('example.jpg');
43 | const action = new GreyAction();
44 | await action.process(ctx, 'grey,0'.split(','));
45 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
46 |
47 | expect(info.format).toBe(sharp.format.jpeg.id);
48 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/indexcrop.test.ts:
--------------------------------------------------------------------------------
1 | import { IndexCropAction } from '../../../src/processor/image/indexcrop';
2 | import { mkctx } from './utils';
3 |
4 |
5 | test('indexcrop action validate', () => {
6 | const action = new IndexCropAction();
7 | expect(() => {
8 | action.validate('indexcrop'.split(','));
9 | }).toThrowError(/IndexCrop param error, e.g: indexcrop,x_100,i_0/);
10 | });
11 |
12 |
13 | test('indexcrop action validate', () => {
14 | const action = new IndexCropAction();
15 | const param1 = action.validate('indexcrop,x_100,i_0'.split(','));
16 | expect(param1).toEqual({
17 | x: 100,
18 | i: 0,
19 | y: 0,
20 | });
21 |
22 | expect(() => {
23 | action.validate('indexcrop,x_-10,i_0'.split(','));
24 | }).toThrowError(/Param error: x value must be greater than 0/);
25 |
26 | expect(() => {
27 | action.validate('indexcrop,y_-10,i_0'.split(','));
28 | }).toThrowError(/Param error: y value must be greater than 0/);
29 |
30 | expect(() => {
31 | action.validate('indexcrop,i_10'.split(','));
32 | }).toThrowError(/IndexCrop param error, e.g: indexcrop,x_100,i_0/);
33 |
34 |
35 | });
36 |
37 | test('indexcrop action validate', () => {
38 | const action = new IndexCropAction();
39 | expect(() => {
40 | action.validate('indexcrop,x_10,y_10'.split(','));
41 | }).toThrowError(/Param error: Cannot enter x and y at the same time/);
42 | });
43 |
44 | test('indexcrop action validate', () => {
45 | const action = new IndexCropAction();
46 | expect(() => {
47 | action.validate('indexcrop,y_-10,i_0'.split(','));
48 | }).toThrowError(/Param error: y value must be greater than 0/);
49 | });
50 |
51 |
52 | test('indexcrop action validate', () => {
53 | const action = new IndexCropAction();
54 | expect(() => {
55 | action.validate('indexcrop,x_10,i_0,abc'.split(','));
56 | }).toThrowError(/Unkown param/);
57 | });
58 |
59 |
60 | test('indexcrop action 01', async () => {
61 | const ctx = await mkctx('example.jpg');
62 | const action = new IndexCropAction();
63 | await action.process(ctx, 'indexcrop,x_100,i_0'.split(','));
64 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
65 | expect(info.width).toBe(100);
66 | });
67 |
68 |
69 | test('indexcrop action', async () => {
70 | const ctx = await mkctx('example.jpg');
71 | const action = new IndexCropAction();
72 | await action.process(ctx, 'indexcrop,y_100,i_0'.split(','));
73 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
74 | expect(info.height).toBe(100);
75 | });
76 |
77 |
78 | test('indexcrop action', async () => {
79 | const ctx = await mkctx('example.jpg');
80 | const action = new IndexCropAction();
81 | await action.process(ctx, 'indexcrop,y_10000,i_0'.split(','));
82 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
83 | expect(info.height).toBeGreaterThan(100);
84 | });
85 |
86 | test('indexcrop action', async () => {
87 | const ctx = await mkctx('example.jpg');
88 | const action = new IndexCropAction();
89 | await action.process(ctx, 'indexcrop,x_10000,i_0'.split(','));
90 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
91 | expect(info.height).toBeGreaterThan(100);
92 | });
93 |
94 | test('indexcrop action', async () => {
95 | const ctx = await mkctx('example.jpg');
96 | const action = new IndexCropAction();
97 | await action.process(ctx, 'indexcrop,x_100,i_100'.split(','));
98 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
99 | expect(info.height).toBeGreaterThan(100);
100 | });
101 |
102 |
103 | test('indexcrop action', async () => {
104 | const ctx = await mkctx('example.jpg');
105 | const action = new IndexCropAction();
106 | await action.process(ctx, 'indexcrop,y_100,i_100'.split(','));
107 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
108 | expect(info.height).toBeGreaterThan(100);
109 | });
110 |
111 | test('indexcrop action', async () => {
112 | const ctx = await mkctx('example.jpg');
113 | const action = new IndexCropAction();
114 | await action.process(ctx, 'indexcrop,x_250,i_1'.split(','));
115 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
116 | expect(info.height).toBeGreaterThan(100);
117 | });
118 |
119 | test('indexcrop action', async () => {
120 | const ctx = await mkctx('example.jpg');
121 | const action = new IndexCropAction();
122 | await action.process(ctx, 'indexcrop,y_200,i_1'.split(','));
123 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
124 | expect(info.height).toBeGreaterThan(201);
125 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/info.test.ts:
--------------------------------------------------------------------------------
1 | import { Features } from '../../../src/processor';
2 | import { InfoAction } from '../../../src/processor/image/info';
3 | import { mkctx } from './utils';
4 |
5 | test('info action validate', () => {
6 | const action = new InfoAction();
7 |
8 | expect(() => {
9 | action.validate('info,-1'.split(','));
10 | }).toThrowError(/Info param error/);
11 |
12 | expect(() => {
13 | action.validate('infox'.split(','));
14 | }).toThrowError(/Info param error/);
15 |
16 | expect(() => {
17 | action.validate('info'.split(','));
18 | }).not.toThrowError(/Info param error/);
19 | });
20 |
21 | test('info action', async () => {
22 | const ctx = await mkctx('example.jpg');
23 | const action = new InfoAction();
24 | await action.process(ctx, 'info'.split(','));
25 |
26 | expect(ctx.features[Features.ReturnInfo]).toBeTruthy();
27 | expect(ctx.info).toEqual({
28 | FileSize: { value: '21839' },
29 | Format: { value: 'jpg' },
30 | ImageHeight: { value: '267' },
31 | ImageWidth: { value: '400' },
32 | });
33 | });
34 |
35 | test('info action on gif', async () => {
36 | const ctx = await mkctx('example.gif');
37 | const action = new InfoAction();
38 | await action.process(ctx, 'info'.split(','));
39 |
40 | expect(ctx.features[Features.ReturnInfo]).toBeTruthy();
41 | expect(ctx.info).toEqual({
42 | FileSize: { value: '21957' },
43 | Format: { value: 'gif' },
44 | ImageHeight: { value: '300' },
45 | ImageWidth: { value: '500' },
46 | });
47 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/interlace.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { InterlaceAction } from '../../../src/processor/image/interlace';
3 | import { mkctx } from './utils';
4 |
5 | test('Interlace action validate', () => {
6 | const action = new InterlaceAction();
7 | const param1 = action.validate('interlace,1'.split(','));
8 | expect(param1).toEqual({
9 | interlace: true,
10 | });
11 |
12 | expect(() => {
13 | action.validate('interlace'.split(','));
14 | }).toThrowError(/Interlace param error, e.g: interlace,1/);
15 |
16 | expect(() => {
17 | action.validate('interlace,xx,22'.split(','));
18 | }).toThrowError(/Interlace param error, e.g: interlace,1/);
19 |
20 | expect(() => {
21 | action.validate('interlace,ab'.split(','));
22 | }).toThrowError(/Interlace must be 0 or 1/);
23 |
24 | expect(() => {
25 | action.validate('interlace,-3'.split(','));
26 | }).toThrowError(/Interlace must be 0 or 1/);
27 |
28 | expect(() => {
29 | action.validate('interlace,3'.split(','));
30 | }).toThrowError(/Interlace must be 0 or 1/);
31 |
32 | });
33 |
34 |
35 | test('interlace,1', async () => {
36 | const ctx = await mkctx('example.jpg');
37 | const action = new InterlaceAction();
38 | await action.process(ctx, 'interlace,1'.split(','));
39 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
40 | expect(info.format).toBe(sharp.format.jpeg.id);
41 | });
42 |
43 |
44 | test('interlace,0', async () => {
45 | const ctx = await mkctx('example.jpg');
46 | const action = new InterlaceAction();
47 | await action.process(ctx, 'interlace,0'.split(','));
48 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
49 | expect(info.format).toBe(sharp.format.jpeg.id);
50 | });
51 |
52 | test('interlace,1 for gif', async () => {
53 | const ctx = await mkctx('example.gif');
54 | const action = new InterlaceAction();
55 | await action.process(ctx, 'interlace,1'.split(','));
56 | const { data, info } = await ctx.image.toBuffer({ resolveWithObject: true });
57 |
58 | expect(info.format).toBe(sharp.format.gif.id);
59 |
60 | const metadata = await sharp(data, { animated: true }).metadata();
61 | expect(metadata.pages).toBe(3);
62 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/quality.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { FormatAction } from '../../../src/processor/image/format';
3 | import * as jpeg from '../../../src/processor/image/jpeg';
4 | import { QualityAction } from '../../../src/processor/image/quality';
5 | import { fixtureStore, mkctx } from './utils';
6 |
7 | test('quality action validate', () => {
8 | const action = new QualityAction();
9 | const param1 = action.validate('quality,q_99,Q_77,,'.split(','));
10 |
11 | expect(param1).toEqual({
12 | q: 99,
13 | Q: 77,
14 | });
15 | expect(() => {
16 | action.validate('quality,xx'.split(','));
17 | }).toThrowError(/Unkown param/);
18 | expect(() => {
19 | action.validate('quality,q_0'.split(','));
20 | }).toThrowError(/Quality must be between 1 and 100/);
21 | expect(() => {
22 | action.validate('quality,q_-1'.split(','));
23 | }).toThrowError(/Quality must be between 1 and 100/);
24 | expect(() => {
25 | action.validate('quality,q_1111'.split(','));
26 | }).toThrowError(/Quality must be between 1 and 100/);
27 | });
28 |
29 |
30 | test('absolute quality action', async () => {
31 | const ctx = await mkctx('example.jpg');
32 | const action = new QualityAction();
33 | await action.process(ctx, 'quality,Q_1'.split(','));
34 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
35 |
36 | expect(info.format).toBe(sharp.format.jpeg.id);
37 | });
38 |
39 | test('relative quality action', async () => {
40 | const ctx = await mkctx('example.jpg');
41 | const action = new QualityAction();
42 | await action.process(ctx, 'quality,q_50'.split(','));
43 | const { data, info } = await ctx.image.toBuffer({ resolveWithObject: true });
44 |
45 | expect(info.format).toBe(sharp.format.jpeg.id);
46 |
47 | expect(jpeg.decode(data).quality).toBe(40);
48 | });
49 |
50 | test('format to webp before quality action', async () => {
51 | const ctx = await mkctx('example.jpg');
52 | const formatAction = new FormatAction();
53 | await formatAction.process(ctx, 'format,webp'.split(','));
54 | const qualityAction = new QualityAction();
55 | await qualityAction.process(ctx, 'quality,q_50'.split(','));
56 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
57 | expect(info.format).toBe(sharp.format.webp.id);
58 | });
59 |
60 | test('estimate image quality', async () => {
61 | const buffer = (await fixtureStore.get('example.jpg')).buffer;
62 | const quality = jpeg.decode(buffer).quality;
63 |
64 | expect(quality).toBe(82);
65 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/rotate.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { RotateAction } from '../../../src/processor/image/rotate';
3 | import { mkctx } from './utils';
4 |
5 | test('quality action validate', () => {
6 | const action = new RotateAction();
7 | const param1 = action.validate('rotate,90'.split(','));
8 | expect(param1).toEqual({
9 | degree: 90,
10 | });
11 |
12 | expect(() => {
13 | action.validate('rotate'.split(','));
14 | }).toThrowError(/Rotate param error, e.g: rotate,90/);
15 |
16 |
17 | expect(() => {
18 | action.validate('rotate,33,abc'.split(','));
19 | }).toThrowError(/Rotate param error, e.g: rotate,90/);
20 |
21 | expect(() => {
22 | action.validate('rotate,abc'.split(','));
23 | }).toThrowError(/Rotate must be between 0 and 360/);
24 | expect(() => {
25 | action.validate('rotate,361'.split(','));
26 | }).toThrowError(/Rotate must be between 0 and 360/);
27 | expect(() => {
28 | action.validate('rotate,-1'.split(','));
29 | }).toThrowError(/Rotate must be between 0 and 360/);
30 |
31 | });
32 |
33 |
34 | test('quality action', async () => {
35 | const ctx = await mkctx('example.jpg');
36 | const action = new RotateAction();
37 | await action.process(ctx, 'interlace,1'.split(','));
38 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
39 | expect(info.format).toBe(sharp.format.jpeg.id);
40 | });
41 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/rounded-corners.test.ts:
--------------------------------------------------------------------------------
1 | import * as Jimp from 'jimp';
2 | import * as sharp from 'sharp';
3 | import { RoundedCornersAction } from '../../../src/processor/image/rounded-corners';
4 | import { mkctx } from './utils';
5 |
6 | test('rounded-corner validate', async () => {
7 | const ctx = await mkctx('example.jpg');
8 |
9 | const action = new RoundedCornersAction();
10 | await action.process(ctx, 'rounded-corners,r_100'.split(','));
11 |
12 | const { data, info } = await ctx.image.png().toBuffer({ resolveWithObject: true });
13 |
14 | const w = info.width;
15 | const h = info.height;
16 |
17 | expect(info.channels).toBe(4);
18 | expect(info.format).toBe(sharp.format.png.id);
19 |
20 | const pic = await Jimp.read(data);
21 | expect(pic.getPixelColor(0, 0)).toBe(0);
22 | expect(pic.getPixelColor(0, w)).toBe(0);
23 | expect(pic.getPixelColor(0, h)).toBe(0);
24 | expect(pic.getPixelColor(w, h)).toBe(0);
25 | });
26 |
27 | test('quality action validate', () => {
28 | const action = new RoundedCornersAction();
29 | const param1 = action.validate('rounded-corners,r_30'.split(','));
30 |
31 | expect(param1).toEqual({ r: 30 });
32 | expect(() => {
33 | action.validate('rounded-corners,r_'.split(','));
34 | }).toThrowError(/must be between 1 and 4096/);
35 | expect(() => {
36 | action.validate('blur,xx'.split(','));
37 | }).toThrowError(/Unkown param/);
38 | });
39 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/sharpen.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { SharpenAction } from '../../../src/processor/image/sharpen';
3 | import { mkctx } from './utils';
4 |
5 | test('quality action validate', () => {
6 | const action = new SharpenAction();
7 | const param1 = action.validate('sharpen,50'.split(','));
8 | expect(param1).toEqual({
9 | sharpen: 50,
10 | });
11 |
12 | expect(() => {
13 | action.validate('sharpen'.split(','));
14 | }).toThrowError(/Sharpen param error, e.g: sharpen,100/);
15 |
16 | expect(() => {
17 | action.validate('sharpen,xx,22'.split(','));
18 | }).toThrowError(/Sharpen param error, e.g: sharpen,100/);
19 |
20 |
21 | expect(() => {
22 | action.validate('sharpen,22'.split(','));
23 | }).toThrowError(/Sharpen be between 50 and 399/);
24 |
25 |
26 | expect(() => {
27 | action.validate('contrast,49'.split(','));
28 | }).toThrowError(/Sharpen be between 50 and 399/);
29 |
30 | expect(() => {
31 | action.validate('contrast,400'.split(','));
32 | }).toThrowError(/Sharpen be between 50 and 399/);
33 |
34 | expect(() => {
35 | action.validate('contrast,100'.split(','));
36 | });
37 |
38 | expect(() => {
39 | action.validate('contrast,60'.split(','));
40 | });
41 | });
42 |
43 |
44 | test('quality action', async () => {
45 | const ctx = await mkctx('example.jpg');
46 | const action = new SharpenAction();
47 | await action.process(ctx, 'sharpen,100'.split(','));
48 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
49 |
50 | expect(info.format).toBe(sharp.format.jpeg.id);
51 | });
52 |
53 |
54 | test('quality action', async () => {
55 | const ctx = await mkctx('example.jpg');
56 | const action = new SharpenAction();
57 | await action.process(ctx, 'sharpen,60'.split(','));
58 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
59 |
60 | expect(info.format).toBe(sharp.format.jpeg.id);
61 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/threshold.test.ts:
--------------------------------------------------------------------------------
1 | // import * as sharp from 'sharp';
2 | import { ThresholdAction } from '../../../src/processor/image/threshold';
3 | import { mkctx } from './utils';
4 |
5 | test(`${ThresholdAction.name} action validate`, () => {
6 | const action = new ThresholdAction();
7 | const param = action.validate('threshold,100'.split(','));
8 | expect(param.threshold).toBe(100);
9 |
10 | expect(() => {
11 | action.validate('threshold'.split(','));
12 | }).toThrow(/Invalid/);
13 |
14 | expect(() => {
15 | action.validate('threshold,-1'.split(','));
16 | }).toThrow(/Invalid.*greater than zero/);
17 |
18 | });
19 |
20 |
21 | test(`${ThresholdAction.name} beforeProcess mask disabled`, async () => {
22 | const ctx = await mkctx('example.jpg', ['threshold,23000']);
23 | const action = new ThresholdAction();
24 |
25 | await action.beforeProcess(ctx, 'threshold,23000'.split(','), 0);
26 | expect(ctx.mask.isDisabled(0)).toBeTruthy();
27 | });
28 |
29 |
30 | test(`${ThresholdAction.name} beforeProcess mask enabled`, async () => {
31 | const ctx = await mkctx('example.jpg', ['threshold,100']);
32 | const action = new ThresholdAction();
33 |
34 | await action.beforeProcess(ctx, 'threshold,100'.split(','), 0);
35 | expect(ctx.mask.isEnabled(0)).toBeTruthy();
36 | });
37 |
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/utils.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as sharp from 'sharp';
3 | import { IImageContext } from '../../../src/processor/image';
4 | import { ActionMask } from '../../../src/processor/image/_base';
5 | import { IBufferStore, LocalStore } from '../../../src/store';
6 |
7 | export const fixtureStore = new LocalStore(path.join(__dirname, '../../fixtures'));
8 |
9 | export async function mkctx(name: string, actions?: string[], bufferStore?: IBufferStore): Promise {
10 | if (!bufferStore) {
11 | bufferStore = fixtureStore;
12 | }
13 | const image = sharp((await bufferStore.get(name)).buffer, {
14 | animated: (name.endsWith('.gif') || name.endsWith('.webp')),
15 | });
16 |
17 | const actions2 = actions ?? [];
18 | const mask = new ActionMask(actions2);
19 | return { uri: name, actions: actions2, mask, image, bufferStore, features: {}, headers: {}, metadata: await image.metadata() };
20 | }
--------------------------------------------------------------------------------
/source/new-image-handler/test/processor/image/watermark.test.ts:
--------------------------------------------------------------------------------
1 | import * as sharp from 'sharp';
2 | import { Features } from '../../../src/processor';
3 | import { WatermarkAction } from '../../../src/processor/image/watermark';
4 | import { mkctx } from './utils';
5 |
6 | const testText = 'hello 世界 !';
7 | const testImgFile = 'aws_logo.png';
8 |
9 | const testTextbuff = Buffer.from(testText, 'utf-8');
10 | const testImgbuff = Buffer.from(testImgFile, 'utf-8');
11 |
12 | const base64Text = testTextbuff.toString('base64');
13 | const base64ImgFile = testImgbuff.toString('base64');
14 |
15 | const testTextParam = `watermark,text_${base64Text},rotate_25,g_se,t_70,color_ff9966`;
16 | const testImgParam = `watermark,image_${base64ImgFile},rotate_25,g_nw,t_70`;
17 | const testMixedParam = `watermark,image_${base64ImgFile},text_${base64Text},g_nw,t_20,align_2,interval_5,size_14`;
18 |
19 | test(testTextParam, async () => {
20 | const ctx = await mkctx('example.jpg');
21 |
22 | const action = new WatermarkAction();
23 | await action.process(ctx, testTextParam.split(','));
24 |
25 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
26 | expect(info.format).toBe(sharp.format.jpeg.id);
27 | });
28 |
29 | test(testImgParam, async () => {
30 | const ctx = await mkctx('example.jpg');
31 |
32 | const action = new WatermarkAction();
33 | await action.process(ctx, testImgParam.split(','));
34 |
35 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
36 | expect(info.format).toBe(sharp.format.jpeg.id);
37 | });
38 |
39 | test(testMixedParam, async () => {
40 | const ctx = await mkctx('example.jpg');
41 |
42 | const action = new WatermarkAction();
43 | await action.process(ctx, testMixedParam.split(','));
44 |
45 | const { info } = await ctx.image.toBuffer({ resolveWithObject: true });
46 | expect(info.format).toBe(sharp.format.jpeg.id);
47 | });
48 |
49 | test(`disable ${Features.ReadAllAnimatedFrames}`, async () => {
50 | const ctx = await mkctx('example.gif');
51 | const action = new WatermarkAction();
52 | action.beforeNewContext(ctx, testMixedParam.split(','));
53 |
54 | expect(ctx.features[Features.ReadAllAnimatedFrames]).toBe(false);
55 | });
--------------------------------------------------------------------------------
/source/new-image-handler/test/store.test.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path';
2 | import * as HttpErrors from 'http-errors';
3 | import { DynamoDBStore, LocalStore, MemKVStore, S3Store } from '../src/store';
4 |
5 | test('local store', async () => {
6 | const store = new LocalStore();
7 | const { buffer } = await store.get(path.join(__dirname, 'fixtures/example.jpg'));
8 |
9 | expect(buffer.length).toBe(21839);
10 | });
11 |
12 | test('local store shortcut', async () => {
13 | const fn = jest.fn();
14 | const store = new LocalStore();
15 | const { buffer } = await store.get(path.join(__dirname, 'fixtures/example.jpg'), fn);
16 |
17 | expect(buffer.length).toBe(21839);
18 | });
19 |
20 |
21 | test('s3 store shortcut', async () => {
22 | const bypass = () => {
23 | // NOTE: This is intended to tell CloudFront to directly access the s3 object without through ECS cluster.
24 | throw new HttpErrors[403]('Please visit s3 directly');
25 | };
26 | const store = new S3Store('sih-input');
27 |
28 | void expect(store.get('Sample-Small-Image-PNG-file-Download.png', bypass))
29 | .rejects
30 | .toThrowError(/Please visit s3/);
31 | void expect(store.get('Sample-Small-Image-PNG-file-Download.png', bypass))
32 | .rejects
33 | .toThrow(HttpErrors.HttpError);
34 | void expect(store.get('Sample-Small-Image-PNG-file-Download.png', bypass))
35 | .rejects
36 | .toThrow(expect.objectContaining({ status: 403 }));
37 | });
38 |
39 |
40 | test('MemKV Store', async () => {
41 | const store = new MemKVStore({
42 | a: { id: 'a', value: 'a' },
43 | b: { id: 'b', value: 'b' },
44 | });
45 |
46 | expect(await store.get('a')).toEqual({ id: 'a', value: 'a' });
47 | expect(await store.get('123')).toEqual({});
48 | });
49 |
50 |
51 | test.skip('s3 store', async () => {
52 | const store = new S3Store('sih-input');
53 | const { buffer, type } = await store.get('Sample-Small-Image-PNG-file-Download.png');
54 |
55 | expect(type).toBe('image/png');
56 | expect(buffer.length).toBe(2678371);
57 | }, 10 * 1000);
58 |
59 |
60 | test.skip('dynamodb store', async () => {
61 | const table = 'serverless-new-image-handler-stack-serverlessecrimagehandlerstackStyleTableE94C4297-PTLOYODP1J7E';
62 | const ddbstore = new DynamoDBStore(table);
63 |
64 | console.log(await ddbstore.get('hello'));
65 | });
--------------------------------------------------------------------------------
/source/new-image-handler/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "alwaysStrict": true,
4 | "declaration": true,
5 | "experimentalDecorators": true,
6 | "inlineSourceMap": true,
7 | "inlineSources": true,
8 | "lib": [
9 | "es2018"
10 | ],
11 | "module": "CommonJS",
12 | "noEmitOnError": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": true,
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "resolveJsonModule": true,
20 | "strict": true,
21 | "strictNullChecks": true,
22 | "strictPropertyInitialization": true,
23 | "stripInternal": true,
24 | "target": "ES2018",
25 | "allowJs": true
26 | },
27 | "include": [
28 | "src/**/*.js",
29 | "src/**/*.ts",
30 | "test/**/*.ts"
31 | ],
32 | "exclude": [
33 | "node_modules"
34 | ],
35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
36 | }
37 |
--------------------------------------------------------------------------------
/source/new-image-handler/tsconfig.jest.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "alwaysStrict": true,
4 | "declaration": true,
5 | "experimentalDecorators": true,
6 | "inlineSourceMap": true,
7 | "inlineSources": true,
8 | "lib": [
9 | "es2018"
10 | ],
11 | "module": "CommonJS",
12 | "noEmitOnError": false,
13 | "noFallthroughCasesInSwitch": true,
14 | "noImplicitAny": true,
15 | "noImplicitReturns": true,
16 | "noImplicitThis": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "resolveJsonModule": true,
20 | "strict": true,
21 | "strictNullChecks": true,
22 | "strictPropertyInitialization": true,
23 | "stripInternal": true,
24 | "target": "ES2018",
25 | "allowJs": true
26 | },
27 | "include": [
28 | "src/**/*.js",
29 | "src/**/*.ts",
30 | "src/**/*.d.ts",
31 | "test/**/*.ts"
32 | ],
33 | "exclude": [
34 | "node_modules"
35 | ],
36 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
37 | }
38 |
--------------------------------------------------------------------------------
/source/new-image-handler/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "lib",
4 | "alwaysStrict": true,
5 | "declaration": true,
6 | "experimentalDecorators": true,
7 | "inlineSourceMap": true,
8 | "inlineSources": true,
9 | "lib": [
10 | "es2018"
11 | ],
12 | "module": "CommonJS",
13 | "noEmitOnError": false,
14 | "noFallthroughCasesInSwitch": true,
15 | "noImplicitAny": true,
16 | "noImplicitReturns": true,
17 | "noImplicitThis": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "resolveJsonModule": true,
21 | "strict": true,
22 | "strictNullChecks": true,
23 | "strictPropertyInitialization": true,
24 | "stripInternal": true,
25 | "target": "ES2018",
26 | "allowJs": true
27 | },
28 | "include": [
29 | "src/**/*.js",
30 | "src/**/*.ts",
31 | "src/**/*.d.ts",
32 | "test/**/*.ts"
33 | ],
34 | "exclude": [],
35 | }
--------------------------------------------------------------------------------