├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── docs └── images │ ├── fill.png │ ├── fit.png │ ├── limit.png │ └── rusty.svg ├── src ├── image │ ├── mod.rs │ └── transform.rs ├── lib.rs └── utils.rs ├── tests ├── input │ ├── Apollo_17_Image_Of_Earth_From_Space.jpeg │ ├── simple.jpg │ ├── simple.png │ └── test_pattern.png ├── output │ └── .gitkeep └── web.rs ├── worker ├── metadata_wasm.json └── worker.js └── wrangler.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | Cargo.lock 3 | **/*.rs.bk 4 | 5 | bin/ 6 | pkg/ 7 | wasm-pack.log 8 | worker/generated/ 9 | 10 | tests/output/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at ag_dubs@cloudflare.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "image-worker" 3 | version = "0.1.0" 4 | authors = ["Pieter Raubenheimer "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | cfg-if = "0.1.2" 15 | wasm-bindgen = "0.2" 16 | serde = "^1.0.59" 17 | serde_derive = "^1.0.59" 18 | serde-wasm-bindgen = "0.1.3" 19 | failure = "0.1.5" 20 | base64 = "0.10.1" 21 | # resvg = "0.7.0" 22 | 23 | # The `console_error_panic_hook` crate provides better debugging of panics by 24 | # logging them with `console.error`. This is great for development, but requires 25 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 26 | # code size when deploying. 27 | console_error_panic_hook = { version = "0.1.1", optional = true } 28 | 29 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 30 | # compared to the default allocator's ~10K. It is slower than the default 31 | # allocator, however. 32 | # 33 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 34 | wee_alloc = { version = "0.4.2", optional = true } 35 | 36 | [dev-dependencies] 37 | wasm-bindgen-test = "0.2" 38 | 39 | [dev-dependencies.wasm-bindgen] 40 | version = "^0.2" 41 | features = ["serde-serialize"] 42 | 43 | [profile.release] 44 | # Tell `rustc` to optimize for small code size. 45 | opt-level = "s" 46 | 47 | [dependencies.image] 48 | version = "0.21.2" 49 | default-features = false 50 | features = ["jpeg", "png_codec", "gif_codec", "webp"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![fill mode](docs/images/rusty.svg) 2 | 3 | # rust-image-worker 4 | 5 | Dynamically crop, resize and cache images, all on the CDN 6 | 7 | ## Why 8 | 9 | The scenario: 10 | 11 | - You need to display images optimized for the layout of your website. 12 | - Perhaps these images get uploaded by your users, or they come from an external image provider. 13 | - You don't want to send images larger than what is neccesary, because you are considerate of users who may be accessing your site from slower connections such as mobiles. 14 | - Maybe you don't know what the size of all the source images are, but you still want them to fit neatly into your design. 15 | - Or, maybe you want to allow your users to change how their profile images are cropped, to move and scale within fixed dimensions, without having to upload again. 16 | 17 | I've used and built such services in the past. I thought it would be a straightforward yet useful thing to build with [Rust](https://www.rust-lang.org) and [WASM](https://webassembly.org)) on [Cloudflare workers](https://www.cloudflare.com/en-gb/products/cloudflare-workers/). 18 | 19 | Rust has great native crates that do not need to pull in shared libraries or call out to other processes. 20 | 21 | With workers we are able to cache the source and resulting images directly on the CDN. 22 | 23 | ### Caveat 24 | 25 | This software is currently used only for demonstration purposes and you should be aware of the [limitations](#limitations) before using it for real. 26 | 27 | ## How to use 28 | 29 | ### Deploying the worker 30 | 31 | 1. Follow instructions to install 🤠[wrangler](https://github.com/cloudflare/wrangler). 32 | 2. Add your [account id and zone id](https://workers.cloudflare.com/docs/quickstart/api-keys/) to the [`wrangler.toml`](wrangler.toml) 33 | 3. Run `$ wrangler publish` 34 | 35 | You should see something like: 36 | 37 | ``` 38 | 🥳 Successfully published your script. 39 | 🥳 Successfully made your script available at image-worker...workers.dev 40 | ✨ Success! Your worker was successfully published. ✨ 41 | ``` 42 | 43 | ### Calling the worker 44 | 45 | You will be able to call the worker at the domain provided,e.g. [http://image-worker...workers.dev](http://factorymethod.uk/image). 46 | 47 | The URL path should be formatted as an image filename with a file extension signifying the target image format. Supported output formats are PNG (`.png`) and JPEG (`.jpg` or `.jpeg`). 48 | 49 | The query parameters should include a combination of: 50 | 51 | - **origin**: the full _URL_ to the source image (required) 52 | - **mode**: one of _fill_, _fit_ and _limit_ (required, see [modes](#modes) for examples) 53 | - **width**, **height**: the desired dimensions (both required when mode is _fill_ or _limit_, either one or both for _fit_) 54 | - **dx**, **dy**: the relative position when the image is cropped, numbers between _-1.0_ (left/top) and _1.0_ (right/bottom) (default: _0.0_, center) 55 | - **scale**: a positive rational number to scale the source image by (default: _1.0_) 56 | - **bg**: a color in [hex triplet](https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) format (default: transparent) 57 | 58 | ## Modes 59 | 60 | ### Fill mode 61 | 62 | The source image is cropped in order to ensure that the full _width_ and _height_ is filled. The source image can be positioned using relative center offset _dx_ and _dy_. 63 | 64 | ![fill mode](docs/images/fill.png) 65 | 66 | Examples: 67 | 68 | | URL | Image | 69 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | 70 | | [https://.../image.png?
mode=fill&
width=180&
height=200&
origin=https://.../test_pattern.png](https://factorymethod.uk/image.png?mode=fill&width=180&height=200&origin=http://factorymethod.uk/test_pattern.png) | ![fill example](https://factorymethod.uk/image.png?mode=fill&width=180&height=200&origin=http://factorymethod.uk/test_pattern.png) | 71 | | [https://.../image.jpg?
mode=fill&
width=180&
height=200&
origin=https://.../Apollo_17.jpeg](https://factorymethod.uk/image.png?mode=fill&width=180&height=200&origin=http://factorymethod.uk/Apollo_17.jpeg) | ![fill example](https://factorymethod.uk/image.jpeg?mode=fill&width=180&height=200&origin=http://factorymethod.uk/Apollo_17.jpeg) | 72 | 73 | ### Fit mode 74 | 75 | The output image is exactly sized according to the given width and height, with no cropping of the source image. The source image can be positioned using relative center offset _dx_ and _dy_. 76 | 77 | ![fit mode](docs/images/fit.png) 78 | 79 | Examples: 80 | 81 | | URL | Image | 82 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | 83 | | [https://.../image.png?
mode=fit&
width=180&
height=200&
bg=abc&
origin=https://.../test_pattern.png](https://factorymethod.uk/image.png?mode=fit&width=180&height=200&bg=abc&origin=http://factorymethod.uk/test_pattern.png) | ![fit example](https://factorymethod.uk/image.png?mode=fit&width=180&height=200&bg=abc&origin=http://factorymethod.uk/test_pattern.png) | 84 | | [https://.../image.jpg?
mode=fit&
width=180&
height=200&
bg=abc&
origin=https://.../Apollo_17.jpeg](https://factorymethod.uk/image.png?mode=fit&width=180&height=200&bg=abc&origin=http://factorymethod.uk/Apollo_17.jpeg) | ![fit example](https://factorymethod.uk/image.jpeg?mode=fit&width=180&height=200&bg=abc&origin=http://factorymethod.uk/Apollo_17.jpeg) | 85 | | Scaled up and cropped to bottom-left
[https://.../image.jpg?
mode=fit&
width=180&
height=200&
scale=1.5&
dx=-1&dy=1&
origin=https://.../Apollo_17.jpeg](https://factorymethod.uk/image.png?mode=fit&width=180&height=200&dx=-1&dy=1&scale=1.5&origin=http://factorymethod.uk/Apollo_17.jpeg) | ![fit example](https://factorymethod.uk/image.jpeg?mode=fit&width=180&height=200&dx=-1&dy=1&scale=1.5&origin=http://factorymethod.uk/Apollo_17.jpeg) | 86 | 87 | ### Limit mode 88 | 89 | The source image is scaled to fit within the given _width_ and _height_. 90 | 91 | ![limit mode](docs/images/limit.png) 92 | 93 | Examples: 94 | 95 | | URL | Image | 96 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | 97 | | [https://.../image.png?
mode=limit&
width=180&
height=200&
origin=https://.../test_pattern.png](https://factorymethod.uk/image.png?mode=limit&width=180&height=200&&origin=http://factorymethod.uk/test_pattern.png) | ![limit example](https://factorymethod.uk/image.png?mode=limit&width=180&height=200&origin=http://factorymethod.uk/test_pattern.png) | 98 | | [https://.../image.jpg?
mode=limit&
width=180&
height=200&
origin=https://.../Apollo_17.jpeg](https://factorymethod.uk/image.png?mode=limit&width=180&height=200&&origin=http://factorymethod.uk/Apollo_17.jpeg) | ![limit example](https://factorymethod.uk/image.jpeg?mode=limit&width=180&height=200&origin=http://factorymethod.uk/Apollo_17.jpeg) | 99 | | Scaled up and cropped to bottom-left
[https://.../image.jpg?
mode=limit&
width=180&
height=200&
scale=1.5&
dx=-1&dy=1&
origin=https://.../Apollo_17.jpeg](https://factorymethod.uk/image.png?mode=limit&width=180&height=200&dx=-1&dy=1&scale=1.5&origin=http://factorymethod.uk/Apollo_17.jpeg) | ![limit example](https://factorymethod.uk/image.jpeg?mode=limit&width=180&height=200&dx=-1&dy=1&scale=1.5&origin=http://factorymethod.uk/Apollo_17.jpeg) | 100 | 101 | ## Limitations 102 | 103 | - Cloudflare workers are [limited](https://developers.cloudflare.com/workers/writing-workers/resource-limits/) in the amount of CPU time they are allowed to take per request (between 5ms for free and 50ms for business/enterprise accounts). This means that large images (> 1000 pixels in width or height), sometimes run out of processing time. 104 | 105 | ## Development 106 | 107 | To run pure Rust tests: 108 | 109 | ``` 110 | $ cargo test 111 | ``` 112 | 113 | And for a headless browser smoke test using Chrome: 114 | 115 | ``` 116 | $ wasm-pack test --headless --chrome 117 | ``` 118 | 119 | ## License 120 | 121 | Apache 2.0 122 | -------------------------------------------------------------------------------- /docs/images/fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/docs/images/fill.png -------------------------------------------------------------------------------- /docs/images/fit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/docs/images/fit.png -------------------------------------------------------------------------------- /docs/images/limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/docs/images/limit.png -------------------------------------------------------------------------------- /docs/images/rusty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Untitled 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/image/mod.rs: -------------------------------------------------------------------------------- 1 | mod transform; 2 | 3 | pub use image::ImageOutputFormat; 4 | 5 | use image::{ 6 | guess_format, load_from_memory, DynamicImage, FilterType, GenericImage, GenericImageView, 7 | ImageFormat, 8 | }; 9 | 10 | pub use transform::{PixelCoords, PixelSize, Transform, TransformMode}; 11 | 12 | pub fn input_to_output_format( 13 | input_format: ImageFormat, 14 | quality: u8, 15 | ) -> Result { 16 | match input_format { 17 | ImageFormat::JPEG => Ok(ImageOutputFormat::JPEG(quality)), 18 | ImageFormat::PNG => Ok(ImageOutputFormat::PNG), 19 | ImageFormat::GIF => Ok(ImageOutputFormat::PNG), 20 | ImageFormat::WEBP => Ok(ImageOutputFormat::PNG), 21 | _ => Err(failure::format_err!("unsupported input format")), 22 | } 23 | } 24 | 25 | pub fn input_format(buffer: &[u8]) -> Result { 26 | guess_format(buffer).map_err(|e| failure::format_err!("could not guess image format {}", e)) 27 | } 28 | 29 | pub fn load(buffer: &[u8]) -> Result { 30 | load_from_memory(buffer).map_err(|e| failure::format_err!("could not load image {}", e)) 31 | } 32 | 33 | pub fn size(image: &DynamicImage) -> PixelSize { 34 | PixelSize { 35 | width: image.width(), 36 | height: image.height(), 37 | } 38 | } 39 | 40 | pub fn process( 41 | image: &mut DynamicImage, 42 | transform: &Transform, 43 | output_format: ImageOutputFormat, 44 | color: Option<[u8; 3]>, 45 | ) -> Result, failure::Error> { 46 | let output_dimensions = transform.get_output_pixel_dimensions(); 47 | let canvas_size = output_dimensions.canvas; 48 | let output_size = output_dimensions.size; 49 | let output_origin = output_dimensions.origin; 50 | 51 | if color.is_some() { 52 | fill(image, color.unwrap()); 53 | } 54 | 55 | let mut resized_image = 56 | image.resize_exact(output_size.width, output_size.height, FilterType::Triangle); 57 | 58 | let mut output_canvas = DynamicImage::new_rgba8(canvas_size.width, canvas_size.height); 59 | 60 | let sub_image_x: u32; 61 | let sub_image_y: u32; 62 | let copied_x: u32; 63 | let copied_y: u32; 64 | 65 | if output_origin.x < 0 { 66 | sub_image_x = output_origin.x.abs() as u32; 67 | copied_x = 0; 68 | } else { 69 | sub_image_x = 0; 70 | copied_x = output_origin.x as u32; 71 | } 72 | 73 | if output_origin.y < 0 { 74 | sub_image_y = output_origin.y.abs() as u32; 75 | copied_y = 0; 76 | } else { 77 | sub_image_y = 0; 78 | copied_y = output_origin.y as u32; 79 | } 80 | 81 | let has_copied = output_canvas.copy_from( 82 | &resized_image.sub_image( 83 | sub_image_x, 84 | sub_image_y, 85 | canvas_size.width.min(output_size.width - sub_image_x), 86 | canvas_size.height.min(output_size.height - sub_image_y), 87 | ), 88 | copied_x, 89 | copied_y, 90 | ); 91 | 92 | if !has_copied { 93 | return Err(failure::format_err!( 94 | "could not place image due to sizing errors", 95 | )); 96 | } 97 | 98 | if color.is_some() { 99 | fill(&mut output_canvas, color.unwrap()); 100 | } 101 | 102 | let mut output: Vec = Vec::new(); 103 | output_canvas 104 | .write_to(&mut output, output_format) 105 | .map(|_| output) 106 | .map_err(|_| failure::format_err!("um")) 107 | } 108 | 109 | fn fill(image: &mut DynamicImage, color_data: [u8; 3]) { 110 | match image { 111 | DynamicImage::ImageRgba8(image_buffer) => { 112 | for mut pixel_mut in image_buffer.pixels_mut() { 113 | let a = pixel_mut.data[3] as f32 / 255.0; 114 | 115 | let r = (a * pixel_mut.data[0] as f32) + ((1.0 - a) * color_data[0] as f32); 116 | let g = (a * pixel_mut.data[1] as f32) + ((1.0 - a) * color_data[1] as f32); 117 | let b = (a * pixel_mut.data[2] as f32) + ((1.0 - a) * color_data[2] as f32); 118 | 119 | pixel_mut.data[0] = r as u8; 120 | pixel_mut.data[1] = g as u8; 121 | pixel_mut.data[2] = b as u8; 122 | pixel_mut.data[3] = 255; 123 | } 124 | } 125 | _ => {} 126 | }; 127 | } 128 | 129 | #[cfg(test)] 130 | mod test { 131 | use super::*; 132 | use std::io::prelude::*; 133 | 134 | #[test] 135 | fn process_a_png_image() { 136 | let mut image = 137 | image::open(std::path::Path::new("./tests/input/test_pattern.png")).unwrap(); 138 | let image_size = size(&image); 139 | 140 | let mut transform = Transform::new( 141 | &image_size, 142 | TransformMode::Fill { 143 | width: 600, 144 | height: 100, 145 | }, 146 | ); 147 | 148 | transform.scale = 0.5; 149 | transform.relative_center_offset.dx = 1.0; 150 | transform.relative_center_offset.dy = -1.0; 151 | 152 | let output = process( 153 | &mut image, 154 | &transform, 155 | ImageOutputFormat::PNG, 156 | Some([100, 200, 100]), 157 | ); 158 | 159 | let mut file = 160 | std::fs::File::create("tests/output/test_pattern_fill_top_right.png").unwrap(); 161 | let result = file.write_all(&output.unwrap()); 162 | result.unwrap(); 163 | } 164 | 165 | #[test] 166 | fn output_a_jpg_image() { 167 | let mut image = 168 | image::open(std::path::Path::new("./tests/input/test_pattern.png")).unwrap(); 169 | let image_size = size(&image); 170 | 171 | let mut transform = Transform::new( 172 | &image_size, 173 | TransformMode::Fit { 174 | width: 100, 175 | height: 100, 176 | }, 177 | ); 178 | 179 | transform.scale = 0.5; 180 | 181 | let output = process( 182 | &mut image, 183 | &transform, 184 | ImageOutputFormat::JPEG(90), 185 | Some([100, 200, 100]), 186 | ); 187 | 188 | let mut file = std::fs::File::create("tests/output/test_pattern_fit.jpg").unwrap(); 189 | let result = file.write_all(&output.unwrap()); 190 | result.unwrap(); 191 | } 192 | 193 | #[test] 194 | fn process_a_jpg_image() { 195 | let mut image = image::open(std::path::Path::new( 196 | "./tests/input/Apollo_17_Image_Of_Earth_From_Space.jpeg", 197 | )) 198 | .unwrap(); 199 | let image_size = size(&image); 200 | 201 | let transform = Transform::new( 202 | &image_size, 203 | TransformMode::Fit { 204 | width: 100, 205 | height: 200, 206 | }, 207 | ); 208 | 209 | let output = process(&mut image, &transform, ImageOutputFormat::JPEG(90), None); 210 | 211 | let mut file = 212 | std::fs::File::create("tests/output/Apollo_17_Image_Of_Earth_From_Space.jpg").unwrap(); 213 | let result = file.write_all(&output.unwrap()); 214 | result.unwrap(); 215 | } 216 | } -------------------------------------------------------------------------------- /src/image/transform.rs: -------------------------------------------------------------------------------- 1 | #[derive(PartialEq, Debug)] 2 | pub enum TransformMode { 3 | Fill { width: u32, height: u32 }, 4 | Fit { width: u32, height: u32 }, 5 | FitWidth(u32), 6 | FitHeight(u32), 7 | Limit { width: u32, height: u32 }, 8 | } 9 | 10 | #[derive(PartialEq, Debug)] 11 | pub struct Coords { 12 | pub x: f32, 13 | pub y: f32, 14 | } 15 | 16 | #[derive(PartialEq, Debug)] 17 | pub struct Size { 18 | pub width: f32, 19 | pub height: f32, 20 | } 21 | 22 | #[derive(PartialEq, Debug)] 23 | pub struct Offset { 24 | pub dx: f32, 25 | pub dy: f32, 26 | } 27 | 28 | #[derive(PartialEq, Debug)] 29 | pub struct Dimensions { 30 | pub size: Size, 31 | pub origin: Coords, 32 | } 33 | 34 | #[derive(PartialEq, Debug)] 35 | pub struct PixelCoords { 36 | pub x: i32, 37 | pub y: i32, 38 | } 39 | 40 | #[derive(PartialEq, Debug)] 41 | pub struct PixelSize { 42 | pub width: u32, 43 | pub height: u32, 44 | } 45 | 46 | #[derive(PartialEq, Debug)] 47 | pub struct PixelDimensions { 48 | pub canvas: PixelSize, 49 | pub size: PixelSize, 50 | pub origin: PixelCoords, 51 | } 52 | 53 | pub struct Transform { 54 | input_size: Size, 55 | canvas_size: Size, 56 | mode: TransformMode, 57 | pub relative_center_offset: Offset, 58 | pub scale: f32, 59 | } 60 | 61 | impl Transform { 62 | pub fn new(input_pixel_size: &PixelSize, mode: TransformMode) -> Self { 63 | let input_size = Size { 64 | width: input_pixel_size.width as f32, 65 | height: input_pixel_size.height as f32, 66 | }; 67 | 68 | let input_ratio = input_size.height / input_size.width; 69 | 70 | let canvas_size = match mode { 71 | TransformMode::Fill { width, height } | TransformMode::Fit { width, height } => Size { 72 | width: width as f32, 73 | height: height as f32, 74 | }, 75 | TransformMode::FitWidth(width) => Size { 76 | width: width as f32, 77 | height: (width as f32) * input_ratio, 78 | }, 79 | TransformMode::FitHeight(height) => Size { 80 | width: (height as f32) / input_ratio, 81 | height: height as f32, 82 | }, 83 | TransformMode::Limit { width, height } => Size { 84 | width: ((height as f32) / input_ratio).min(width as f32), 85 | height: ((width as f32) * input_ratio).min(height as f32), 86 | }, 87 | }; 88 | 89 | Transform { 90 | input_size: input_size, 91 | canvas_size: canvas_size, 92 | mode: mode, 93 | relative_center_offset: Offset { dx: 0.0, dy: 0.0 }, 94 | scale: 1.0, 95 | } 96 | } 97 | 98 | fn get_output_size(&self) -> Size { 99 | let input_size = &self.input_size; 100 | let input_ratio = input_size.height / input_size.width; 101 | 102 | let canvas_size = &self.canvas_size; 103 | let canvas_ratio = canvas_size.height / canvas_size.width; 104 | 105 | let mut output_size = Size { 106 | width: 0.0, 107 | height: 0.0, 108 | }; 109 | 110 | match &self.mode { 111 | TransformMode::Fill { 112 | width: _, 113 | height: _, 114 | } => { 115 | if canvas_ratio > input_ratio { 116 | output_size.height = canvas_size.height 117 | } else { 118 | output_size.width = canvas_size.width 119 | } 120 | } 121 | _ => { 122 | if input_ratio < 1.0 && input_ratio < canvas_ratio { 123 | output_size.width = canvas_size.width 124 | } else { 125 | output_size.height = canvas_size.height 126 | } 127 | } 128 | } 129 | 130 | if output_size.height > 0.0 { 131 | let ratio = output_size.height / input_size.height; 132 | 133 | output_size.width = ratio * input_size.width; 134 | } else { 135 | let ratio = output_size.width / input_size.width; 136 | 137 | output_size.height = ratio * input_size.height; 138 | } 139 | 140 | output_size.width = output_size.width * self.scale; 141 | output_size.height = output_size.height * self.scale; 142 | 143 | output_size 144 | } 145 | 146 | fn get_output_origin(&self, output_size: &Size) -> Coords { 147 | let center = Coords { 148 | x: output_size.width / 2.0, 149 | y: output_size.height / 2.0, 150 | }; 151 | 152 | let canvas_center = Coords { 153 | x: self.canvas_size.width / 2.0, 154 | y: self.canvas_size.height / 2.0, 155 | }; 156 | 157 | Coords { 158 | x: canvas_center.x - center.x 159 | + (canvas_center.x - center.x) * self.relative_center_offset.dx, 160 | y: canvas_center.y - center.y 161 | + (canvas_center.y - center.y) * self.relative_center_offset.dy, 162 | } 163 | } 164 | 165 | fn get_output_dimensions(&self) -> Dimensions { 166 | let output_size = self.get_output_size(); 167 | 168 | Dimensions { 169 | origin: self.get_output_origin(&output_size), 170 | size: output_size, 171 | } 172 | } 173 | 174 | pub fn get_output_pixel_dimensions(&self) -> PixelDimensions { 175 | let output_dimensions = self.get_output_dimensions(); 176 | 177 | PixelDimensions { 178 | canvas: PixelSize { 179 | width: self.canvas_size.width.round() as u32, 180 | height: self.canvas_size.height.round() as u32, 181 | }, 182 | size: PixelSize { 183 | width: output_dimensions.size.width.round() as u32, 184 | height: output_dimensions.size.height.round() as u32, 185 | }, 186 | origin: PixelCoords { 187 | x: output_dimensions.origin.x.round() as i32, 188 | y: output_dimensions.origin.y.round() as i32, 189 | }, 190 | } 191 | } 192 | } 193 | 194 | #[cfg(test)] 195 | mod test { 196 | use super::{ 197 | Coords, Dimensions, PixelCoords, PixelDimensions, PixelSize, Size, Transform, TransformMode, 198 | }; 199 | 200 | #[test] 201 | fn fixed_ratios() { 202 | let transform = Transform::new( 203 | &PixelSize { 204 | width: 100, 205 | height: 100, 206 | }, 207 | TransformMode::Fill { 208 | width: 50, 209 | height: 50, 210 | }, 211 | ); 212 | 213 | let output_dimensions = transform.get_output_dimensions(); 214 | 215 | assert_eq!( 216 | output_dimensions, 217 | Dimensions { 218 | origin: Coords { x: 0.0, y: 0.0 }, 219 | size: Size { 220 | width: 50.0, 221 | height: 50.0 222 | } 223 | } 224 | ); 225 | } 226 | 227 | #[test] 228 | fn portrait_input_and_portrait_canvas() { 229 | let transform = Transform::new( 230 | &PixelSize { 231 | width: 200, 232 | height: 300, 233 | }, 234 | TransformMode::Fill { 235 | width: 20, 236 | height: 25, 237 | }, 238 | ); 239 | 240 | let output_dimensions = transform.get_output_dimensions(); 241 | 242 | assert_eq!( 243 | output_dimensions, 244 | Dimensions { 245 | origin: Coords { x: 0.0, y: -2.5 }, 246 | size: Size { 247 | width: 20.0, 248 | height: 30.0 249 | } 250 | } 251 | ); 252 | } 253 | 254 | #[test] 255 | fn portrait_input_and_longer_portrait_canvas() { 256 | let transform = Transform::new( 257 | &PixelSize { 258 | width: 200, 259 | height: 300, 260 | }, 261 | TransformMode::Fill { 262 | width: 20, 263 | height: 40, 264 | }, 265 | ); 266 | 267 | let output_dimensions = transform.get_output_dimensions(); 268 | 269 | assert_eq!( 270 | output_dimensions, 271 | Dimensions { 272 | origin: Coords { 273 | x: -3.333334, 274 | y: 0.0 275 | }, 276 | size: Size { 277 | width: 26.666668, 278 | height: 40.0 279 | } 280 | } 281 | ); 282 | } 283 | 284 | #[test] 285 | fn square_input_and_landscape_canvas() { 286 | let transform = Transform::new( 287 | &PixelSize { 288 | width: 300, 289 | height: 300, 290 | }, 291 | TransformMode::Fill { 292 | width: 200, 293 | height: 100, 294 | }, 295 | ); 296 | 297 | let output_dimensions = transform.get_output_dimensions(); 298 | 299 | assert_eq!( 300 | output_dimensions, 301 | Dimensions { 302 | origin: Coords { x: 0.0, y: -50.0 }, 303 | size: Size { 304 | width: 200.0, 305 | height: 200.0 306 | } 307 | } 308 | ); 309 | } 310 | 311 | #[test] 312 | fn positions_output_with_relative_center() { 313 | let mut transform = Transform::new( 314 | &PixelSize { 315 | width: 200, 316 | height: 300, 317 | }, 318 | TransformMode::Fill { 319 | width: 20, 320 | height: 25, 321 | }, 322 | ); 323 | 324 | transform.relative_center_offset.dy = -1.0; // Top 325 | transform.relative_center_offset.dx = 0.0; // Center 326 | 327 | let mut output_dimensions = transform.get_output_dimensions(); 328 | 329 | assert_eq!( 330 | output_dimensions, 331 | Dimensions { 332 | origin: Coords { x: 0.0, y: 0.0 }, 333 | size: Size { 334 | width: 20.0, 335 | height: 30.0 336 | } 337 | } 338 | ); 339 | 340 | transform.relative_center_offset.dy = 0.0; // Center 341 | transform.relative_center_offset.dx = 1.0; // Right 342 | 343 | output_dimensions = transform.get_output_dimensions(); 344 | assert_eq!( 345 | output_dimensions, 346 | Dimensions { 347 | origin: Coords { x: 0.0, y: -2.5 }, 348 | size: Size { 349 | width: 20.0, 350 | height: 30.0 351 | } 352 | } 353 | ); 354 | } 355 | 356 | #[test] 357 | fn fits_landscape_image() { 358 | let transform = Transform::new( 359 | &PixelSize { 360 | width: 300, 361 | height: 200, 362 | }, 363 | TransformMode::Fit { 364 | width: 20, 365 | height: 30, 366 | }, 367 | ); 368 | 369 | let output_dimensions = transform.get_output_dimensions(); 370 | 371 | assert_eq!( 372 | output_dimensions, 373 | Dimensions { 374 | origin: Coords { 375 | x: 0.0, 376 | y: 8.333333 377 | }, 378 | size: Size { 379 | width: 20.0, 380 | height: 13.333334 381 | } 382 | } 383 | ); 384 | } 385 | 386 | #[test] 387 | fn fits_to_width_only() { 388 | let transform = Transform::new( 389 | &PixelSize { 390 | width: 300, 391 | height: 200, 392 | }, 393 | TransformMode::FitWidth(20), 394 | ); 395 | 396 | let output_dimensions = transform.get_output_pixel_dimensions(); 397 | 398 | assert_eq!( 399 | output_dimensions, 400 | PixelDimensions { 401 | canvas: PixelSize { 402 | width: 20, 403 | height: 13 404 | }, 405 | origin: PixelCoords { x: 0, y: 0 }, 406 | size: PixelSize { 407 | width: 20, 408 | height: 13 409 | } 410 | } 411 | ); 412 | } 413 | 414 | #[test] 415 | fn fits_to_height_only() { 416 | let transform = Transform::new( 417 | &PixelSize { 418 | width: 300, 419 | height: 200, 420 | }, 421 | TransformMode::FitHeight(20), 422 | ); 423 | 424 | let output_dimensions = transform.get_output_pixel_dimensions(); 425 | 426 | assert_eq!( 427 | output_dimensions, 428 | PixelDimensions { 429 | canvas: PixelSize { 430 | width: 30, 431 | height: 20 432 | }, 433 | origin: PixelCoords { x: 0, y: 0 }, 434 | size: PixelSize { 435 | width: 30, 436 | height: 20 437 | } 438 | } 439 | ); 440 | } 441 | 442 | #[test] 443 | fn limit_to_scaled_square() { 444 | let mut transform = Transform::new( 445 | &PixelSize { 446 | width: 200, 447 | height: 300, 448 | }, 449 | TransformMode::Limit { 450 | width: 60, 451 | height: 60, 452 | }, 453 | ); 454 | 455 | transform.scale = 1.2; 456 | 457 | let output_dimensions = transform.get_output_dimensions(); 458 | 459 | assert_eq!( 460 | output_dimensions, 461 | Dimensions { 462 | origin: Coords { x: -4.0, y: -6.0 }, 463 | size: Size { 464 | width: 48.0, 465 | height: 72.0 466 | } 467 | } 468 | ); 469 | } 470 | } 471 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate cfg_if; 2 | extern crate wasm_bindgen; 3 | 4 | mod image; 5 | mod utils; 6 | 7 | use cfg_if::cfg_if; 8 | use serde_wasm_bindgen::from_value; 9 | use wasm_bindgen::prelude::*; 10 | 11 | #[macro_use] 12 | extern crate serde_derive; 13 | 14 | cfg_if! { 15 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 16 | // allocator. 17 | if #[cfg(feature = "wee_alloc")] { 18 | extern crate wee_alloc; 19 | #[global_allocator] 20 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 21 | } 22 | } 23 | 24 | fn positive_int_value(value: u32) -> Option { 25 | if value > 0 { 26 | Some(value) 27 | } else { 28 | None 29 | } 30 | } 31 | 32 | #[derive(Serialize, Deserialize)] 33 | struct ProcessImageParams { 34 | bg: Vec, 35 | dx: f32, 36 | dy: f32, 37 | format: String, 38 | height: u32, 39 | mode: String, 40 | quality: u8, 41 | scale: f32, 42 | width: u32, 43 | } 44 | 45 | fn error_to_js_value(e: failure::Error) -> JsValue { 46 | JsValue::from_str(&e.to_string()) 47 | } 48 | 49 | 50 | #[wasm_bindgen] 51 | pub fn process_image(buffer: &[u8], params_value: JsValue) -> Result, JsValue> { 52 | console_error_panic_hook::set_once(); 53 | 54 | let params: ProcessImageParams = from_value(params_value)?; 55 | 56 | let transform_mode = string_to_transform_mode( 57 | ¶ms.mode, 58 | positive_int_value(params.width), 59 | positive_int_value(params.height), 60 | ) 61 | .map_err(|e| JsValue::from_str(&e.to_string()))?; 62 | 63 | let output_format = match string_to_output_format(¶ms.format, params.quality) { 64 | None => { 65 | let input_format = image::input_format(buffer).map_err(error_to_js_value)?; 66 | image::input_to_output_format(input_format, params.quality) 67 | .map_err(error_to_js_value)? 68 | } 69 | Some(output_format) => output_format, 70 | }; 71 | 72 | let mut image = image::load(buffer).map_err(|e| JsValue::from(e.to_string()))?; 73 | let image_size = image::size(&image); 74 | 75 | let mut transform = image::Transform::new(&image_size, transform_mode); 76 | transform.relative_center_offset.dx = params.dx; 77 | transform.relative_center_offset.dy = params.dy; 78 | transform.scale = params.scale; 79 | 80 | let color_option = if params.bg.is_empty() { 81 | None 82 | } else { 83 | Some([params.bg[0], params.bg[1], params.bg[2]]) 84 | }; 85 | 86 | let mut output = image::process(&mut image, &transform, output_format.clone(), color_option) 87 | .map_err(error_to_js_value)?; 88 | 89 | output.push(output_format_to_key(output_format)); 90 | 91 | Ok(output) 92 | } 93 | 94 | fn string_to_transform_mode( 95 | mode_string: &str, 96 | width: Option, 97 | height: Option, 98 | ) -> Result { 99 | if mode_string == "fit" { 100 | if width.and(height).is_some() { 101 | Ok(image::TransformMode::Fit { 102 | width: width.unwrap(), 103 | height: height.unwrap(), 104 | }) 105 | } else if width.is_some() { 106 | Ok(image::TransformMode::FitWidth(width.unwrap())) 107 | } else { 108 | Ok(image::TransformMode::FitHeight(height.unwrap())) 109 | } 110 | } else if width.and(height).is_some() { 111 | match mode_string { 112 | "fill" => Ok(image::TransformMode::Fill { 113 | width: width.unwrap(), 114 | height: height.unwrap(), 115 | }), 116 | "limit" => Ok(image::TransformMode::Limit { 117 | width: width.unwrap(), 118 | height: height.unwrap(), 119 | }), 120 | _ => Err(failure::format_err!("unknown mode")), 121 | } 122 | } else { 123 | Err(failure::format_err!("mode needs width and height")) 124 | } 125 | } 126 | 127 | fn string_to_output_format(format_string: &str, quality: u8) -> Option { 128 | match format_string { 129 | "png" => Some(image::ImageOutputFormat::PNG), 130 | "jpg" => Some(image::ImageOutputFormat::JPEG(quality)), 131 | _ => None, 132 | } 133 | } 134 | 135 | fn output_format_to_key(output_format: image::ImageOutputFormat) -> u8 { 136 | match output_format { 137 | image::ImageOutputFormat::PNG => 0, 138 | image::ImageOutputFormat::JPEG(_) => 1, 139 | _ => unimplemented!(), 140 | } 141 | } -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use cfg_if::cfg_if; 2 | 3 | cfg_if! { 4 | // When the `console_error_panic_hook` feature is enabled, we can call the 5 | // `set_panic_hook` function at least once during initialization, and then 6 | // we will get better error messages if our code ever panics. 7 | // 8 | // For more details see 9 | // https://github.com/rustwasm/console_error_panic_hook#readme 10 | if #[cfg(feature = "console_error_panic_hook")] { 11 | extern crate console_error_panic_hook; 12 | pub use self::console_error_panic_hook::set_once as set_panic_hook; 13 | } else { 14 | #[inline] 15 | pub fn set_panic_hook() {} 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/input/Apollo_17_Image_Of_Earth_From_Space.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/tests/input/Apollo_17_Image_Of_Earth_From_Space.jpeg -------------------------------------------------------------------------------- /tests/input/simple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/tests/input/simple.jpg -------------------------------------------------------------------------------- /tests/input/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/tests/input/simple.png -------------------------------------------------------------------------------- /tests/input/test_pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/tests/input/test_pattern.png -------------------------------------------------------------------------------- /tests/output/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupiter/rust-image-worker/02c3908e393aa17b0d17c48b15ef5aed0039a480/tests/output/.gitkeep -------------------------------------------------------------------------------- /tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | // NOTE: Other tests are within the relevant source files 3 | 4 | #![cfg(target_arch = "wasm32")] 5 | 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | use base64::decode; 11 | use image_worker::process_image; 12 | use wasm_bindgen::JsValue; 13 | 14 | #[macro_use] 15 | extern crate serde_derive; 16 | 17 | #[cfg(test)] 18 | enum TestImage { 19 | Jpeg, 20 | Png, 21 | } 22 | 23 | #[cfg(test)] 24 | impl TestImage { 25 | fn get_vec(&self) -> Vec { 26 | decode(match self { 27 | TestImage::Jpeg => "/9j/4AAQSkZJRgABAQAASABIAAD/4QDKRXhpZgAATU0AKgAAAAgABwESAAMAAAABAAEAAAEaAAUAAAABAAAAYgEbAAUAAAABAAAAagEoAAMAAAABAAIAAAExAAIAAAARAAAAcgEyAAIAAAAUAAAAhIdpAAQAAAABAAAAmAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciAzLjguNQAAMjAxOTowNzoxMSAxMjowNzo0MgAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAyKADAAQAAAABAAAAyAAAAAD/4QmSaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA1LjQuMCI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJQaXhlbG1hdG9yIDMuOC41IiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0wNy0xMVQxMjowNzo0MiIvPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDw/eHBhY2tldCBlbmQ9InciPz4A/+0AOFBob3Rvc2hvcCAzLjAAOEJJTQQE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", 28 | TestImage::Png => "iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAIAAAAiOjnJAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAA6ppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgICAgICAgICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxOS0wNy0xMVQxMjowNzo0NTwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGl4ZWxtYXRvciAzLjguNTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICAgICA8dGlmZjpDb21wcmVzc2lvbj4wPC90aWZmOkNvbXByZXNzaW9uPgogICAgICAgICA8dGlmZjpSZXNvbHV0aW9uVW5pdD4yPC90aWZmOlJlc29sdXRpb25Vbml0PgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjwvdGlmZjpZUmVzb2x1dGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzI8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yMDA8L2V4aWY6UGl4ZWxYRGltZW5zaW9uPgogICAgICAgICA8ZXhpZjpDb2xvclNwYWNlPjE8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjIwMDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgr0M5ySAAAEs0lEQVR4Ae3UwYlVQRRFUZVOSOhsBCMwrc5G6JAUpzXYHw71Ri5nj/NvgYtNf/3z89cX/wjcFvh2+0HvEfgnICwdPCIgrEdYPSosDTwiIKxHWD0qLA08IiCsR1g9KiwNPCIgrEdYPSosDTwi8Pby1d8/vr/8jR/8bwLvH5/9X/YXq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so4CwRjhnLSCs9rGOAsIa4Zy1gLDaxzoKCGuEc9YCwmof6yggrBHOWQsIq32so8Dby7v3j8+Xv/EDAoeAv1gHiM87AsK64+iVQ0BYB4jPOwLCuuPolUNAWAeIzzsCwrrj6JVDQFgHiM87AsK64+iVQ0BYB4jPOwLCuuPolUPgL+NCCAXjUgXAAAAAAElFTkSuQmCC", 29 | }).unwrap() 30 | } 31 | } 32 | 33 | #[derive(Serialize)] 34 | struct ProcessImageParams { 35 | bg: Vec, 36 | dx: f32, 37 | dy: f32, 38 | format: String, 39 | height: u32, 40 | mode: String, 41 | quality: u8, 42 | scale: f32, 43 | width: u32, 44 | } 45 | 46 | #[wasm_bindgen_test] 47 | fn process_jpeg_image_in_browser() { 48 | let data = TestImage::Jpeg.get_vec(); 49 | 50 | assert_eq!(data.len(), 6391); 51 | 52 | let bytes = &data[..data.len()]; 53 | 54 | process_image( 55 | &bytes, 56 | JsValue::from_serde(&ProcessImageParams { 57 | bg: vec![], 58 | dx: 0.0, 59 | dy: 0.0, 60 | format: "jpeg".to_string(), 61 | height: 100, 62 | mode: "fill".to_string(), 63 | quality: 90, 64 | scale: 1.0, 65 | width: 50, 66 | }) 67 | .unwrap(), 68 | ) 69 | .unwrap(); 70 | } 71 | 72 | #[wasm_bindgen_test] 73 | fn process_png_image_in_browser() { 74 | let data = TestImage::Png.get_vec(); 75 | 76 | assert_eq!(data.len(), 2244); 77 | 78 | let bytes = &data[..data.len()]; 79 | 80 | process_image( 81 | &bytes, 82 | JsValue::from_serde(&ProcessImageParams { 83 | bg: vec![], 84 | dx: 0.0, 85 | dy: 0.0, 86 | format: "png".to_string(), 87 | height: 100, 88 | mode: "fill".to_string(), 89 | quality: 90, 90 | scale: 1.0, 91 | width: 50, 92 | }) 93 | .unwrap(), 94 | ) 95 | .unwrap(); 96 | } -------------------------------------------------------------------------------- /worker/metadata_wasm.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_part": "script", 3 | "bindings": [ 4 | { 5 | "name": "wasm", 6 | "type": "wasm_module", 7 | "part": "wasmprogram" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /worker/worker.js: -------------------------------------------------------------------------------- 1 | addEventListener("fetch", event => { 2 | event.respondWith(handleRequest(event.request)); 3 | }); 4 | 5 | async function handleRequest(req) { 6 | let res; 7 | 8 | if (req.method !== "GET") { 9 | res = new Response("http method not allowed", { status: 405 }); 10 | res.headers.set("Content-type", "text/plain"); 11 | return res; 12 | } 13 | 14 | let cache = caches.default; 15 | res = await cache.match(req); 16 | if (res) { 17 | return res; 18 | } 19 | 20 | const params = getParams(req); 21 | 22 | if (params.errors.length) { 23 | res = new Response(params.errors.join("\r\n"), { status: 400 }); 24 | res.headers.set("Content-type", "text/plain"); 25 | return res; 26 | } 27 | 28 | const { process_image } = wasm_bindgen; 29 | 30 | let originReq = new Request(params.origin.toString(), req); 31 | let [originRes] = await Promise.all([ 32 | cache.match(originReq), 33 | wasm_bindgen(wasm) 34 | ]); 35 | 36 | try { 37 | let originResToCache; 38 | if (!originRes) { 39 | originRes = await fetch(originReq); 40 | originResToCache = originRes.clone(); 41 | } 42 | 43 | const data = await originRes.arrayBuffer(); 44 | const output = process_image(new Uint8Array(data), params); 45 | const output_format = output.slice(-1); 46 | 47 | res = new Response(output.slice(0, -1), { status: 200 }); 48 | res.headers.set("Content-type", getMimeType(VALID_FORMATS[output_format])); 49 | 50 | cache.put(req, res.clone()); 51 | if (originResToCache) { 52 | cache.put(originReq, originResToCache); 53 | } 54 | } catch (e) { 55 | res = new Response(e.toString(), { status: 200 }); 56 | res.headers.set("Content-type", "text/plain"); 57 | } 58 | return res; 59 | } 60 | 61 | const VALID_FORMATS = ["png", "jpg", "jpeg"]; 62 | const VALID_MODES = ["fill", "fit", "limit"]; 63 | 64 | function getParams(req) { 65 | const errors = []; 66 | const params = { 67 | bg: [], 68 | dx: 0, 69 | dy: 0, 70 | errors, 71 | format: "", 72 | height: 0, 73 | mode: "", 74 | origin: "", 75 | quality: 90, 76 | scale: 1, 77 | width: 0 78 | }; 79 | 80 | const reqUrl = new URL(req.url); 81 | const searchParams = reqUrl.searchParams; 82 | 83 | const format = getUrlExt(reqUrl); 84 | if (format) { 85 | params.format = format; 86 | if (!VALID_FORMATS.includes(params.format)) { 87 | errors.push( 88 | `image .extension must be one of ${format} ${VALID_FORMATS.join(", ")}` 89 | ); 90 | } 91 | } 92 | 93 | if (searchParams.has("quality")) { 94 | params.quality = parseInt(searchParams.get("quality"), 10); 95 | if (params.quality > 100 || params.quality < 40) { 96 | errors.push("quality must be a number between 40 and 100"); 97 | } 98 | } 99 | 100 | if (searchParams.has("origin")) { 101 | try { 102 | params.origin = new URL(searchParams.get("origin")); 103 | } catch (_) {} 104 | } 105 | 106 | if (!params.origin) { 107 | errors.push("origin must be a valid image URL"); 108 | } 109 | 110 | if (searchParams.has("width")) { 111 | params.width = parseInt(searchParams.get("width"), 10); 112 | if (!(params.width > -1)) { 113 | errors.push("width must be a positive number"); 114 | } 115 | } 116 | 117 | if (searchParams.has("height")) { 118 | params.height = parseInt(searchParams.get("height"), 10); 119 | if (!(params.height > -1)) { 120 | errors.push("height must be a positive number"); 121 | } 122 | } 123 | 124 | if (!(params.width || params.height)) { 125 | errors.push("width and/or height must be provided"); 126 | } 127 | 128 | if (searchParams.has("dx")) { 129 | params.dx = parseFloat(searchParams.get("dx")); 130 | if (!(params.dx >= -1 || params.dx <= 1)) { 131 | errors.push("dx must be a number between -1.0 and 1.0 (default: 0)"); 132 | } 133 | } 134 | 135 | if (searchParams.has("dy")) { 136 | params.dy = parseFloat(searchParams.get("dy")); 137 | if (!(params.dy >= -1 || params.dy <= 1)) { 138 | errors.push("dy must be between -1.0 and 1.0 (default: 0)"); 139 | } 140 | } 141 | 142 | if (searchParams.has("scale")) { 143 | params.scale = parseFloat(searchParams.get("scale")); 144 | if (!(params.scale > 0 || params.scale <= 10)) { 145 | errors.push("scale must be a non-zero number up to 10 (default: 1)"); 146 | } 147 | } 148 | 149 | if (searchParams.has("mode")) { 150 | params.mode = String(searchParams.get("mode").toLowerCase()); 151 | } 152 | 153 | if (!VALID_MODES.includes(params.mode)) { 154 | errors.push(`mode must be one of ${VALID_MODES.join(", ")}`); 155 | } 156 | 157 | if (searchParams.has("bg")) { 158 | const bg = getColor(String(searchParams.get("bg")).toLowerCase()); 159 | if (bg) { 160 | params.bg = bg; 161 | } else { 162 | errors.push("bg must be a valid hex color between 000 and ffffff"); 163 | } 164 | } 165 | 166 | return params; 167 | } 168 | 169 | function getUrlExt(url) { 170 | const extMatch = url.pathname.match(/\.(\w+)$/); 171 | return extMatch && extMatch[1].toLowerCase(); 172 | } 173 | 174 | function getColor(hexStr) { 175 | if (hexStr.length === 3) { 176 | hexStr = hexStr 177 | .split("") 178 | .map(c => c + c) 179 | .join(""); 180 | } 181 | if (hexStr.length === 6) { 182 | const output = []; 183 | for (let i = 0; i < 3; i++) { 184 | const hex = parseInt(hexStr.slice(0, 2), 16); 185 | if (hex === NaN) { 186 | return; 187 | } 188 | output.push(hex); 189 | } 190 | return output; 191 | } 192 | } 193 | 194 | function getMimeType(format) { 195 | return ( 196 | { 197 | png: "image/png", 198 | jpg: "image/jpeg" 199 | }[format] || "application/octet-stream" 200 | ); 201 | } 202 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | account_id = "" 2 | name = "image-worker" 3 | private = false 4 | route = "/image*" 5 | type = "rust" 6 | zone_id = "" 7 | --------------------------------------------------------------------------------