├── contrib ├── dropon.png ├── modjpeg.conf.in └── run.sh ├── .gitignore ├── config ├── LICENSE ├── Dockerfile ├── README.md └── ngx_http_jpeg_filter_module.c /contrib/dropon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ioppermann/modjpeg-nginx/HEAD/contrib/dropon.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Object files 4 | *.o 5 | *.ko 6 | *.obj 7 | *.elf 8 | 9 | # Precompiled Headers 10 | *.gch 11 | *.pch 12 | 13 | # Libraries 14 | *.lib 15 | *.a 16 | *.la 17 | *.lo 18 | 19 | # Shared objects (inc. Windows DLLs) 20 | *.dll 21 | *.so 22 | *.so.* 23 | *.dylib 24 | 25 | # Executables 26 | *.exe 27 | *.out 28 | *.app 29 | *.i*86 30 | *.x86_64 31 | *.hex 32 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | ngx_addon_name=ngx_http_jpeg_filter_module 2 | 3 | if test -n "$ngx_module_link"; then 4 | ngx_module_type=HTTP_AUX_FILTER 5 | ngx_module_name=ngx_http_jpeg_filter_module 6 | ngx_module_srcs="$ngx_addon_dir/ngx_http_jpeg_filter_module.c" 7 | ngx_module_libs="-lmodjpeg" 8 | 9 | . auto/module 10 | else 11 | HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES ngx_http_jpeg_filter_module" 12 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_jpeg_filter_module.c" 13 | CORE_LIBS="$CORE_LIBS -lmodjpeg" 14 | fi 15 | -------------------------------------------------------------------------------- /contrib/modjpeg.conf.in: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | daemon off; 4 | 5 | error_log stderr info; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include mime.types; 13 | default_type application/octet-stream; 14 | 15 | access_log off; 16 | 17 | sendfile on; 18 | 19 | keepalive_timeout 65; 20 | 21 | server { 22 | listen 80; 23 | server_name localhost; 24 | 25 | root /images; 26 | 27 | location / { 28 | autoindex on; 29 | 30 | jpeg_filter on; 31 | jpeg_filter_graceful %MJ_GRACEFUL%; 32 | jpeg_filter_buffer %MJ_BUFFER%; 33 | jpeg_filter_max_pixel %MJ_MAX_PIXEL%; 34 | 35 | jpeg_filter_dropon_align %MJ_DROPON_ALIGN%; 36 | jpeg_filter_dropon_offset %MJ_DROPON_OFFSET%; 37 | jpeg_filter_dropon_file %MJ_DROPON_FILE%; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /contrib/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "$MJ_GRACEFUL" != "on" -a "$MJ_GRACEFUL" != "off" ]; then 4 | MJ_GRACEFUL=on 5 | fi 6 | 7 | if [ "$MJ_BUFFER" = "" ]; then 8 | MJ_BUFFER=10M 9 | fi 10 | 11 | if [ "$MJ_MAX_PIXEL" = "" ]; then 12 | MJ_MAX_PIXEL=0 13 | fi 14 | 15 | if [ "$MJ_DROPON_ALIGN" = "" ]; then 16 | MJ_DROPON_ALIGN="top left" 17 | fi 18 | 19 | if [ "$MJ_DROPON_OFFSET" = "" ]; then 20 | MJ_DROPON_OFFSET="0 0" 21 | fi 22 | 23 | if [ "$MJ_DROPON_FILE" = "" ]; then 24 | MJ_DROPON_FILE="/usr/local/nginx/conf/dropon.png" 25 | fi 26 | 27 | cp /usr/local/nginx/conf/modjpeg.conf.in /usr/local/nginx/conf/modjpeg.conf 28 | 29 | sed -i"" "s@%MJ_GRACEFUL%@$MJ_GRACEFUL@g" /usr/local/nginx/conf/modjpeg.conf 30 | sed -i"" "s@%MJ_BUFFER%@$MJ_BUFFER@g" /usr/local/nginx/conf/modjpeg.conf 31 | sed -i"" "s@%MJ_MAX_PIXEL%@$MJ_MAX_PIXEL@g" /usr/local/nginx/conf/modjpeg.conf 32 | sed -i"" "s@%MJ_DROPON_ALIGN%@$MJ_DROPON_ALIGN@g" /usr/local/nginx/conf/modjpeg.conf 33 | sed -i"" "s@%MJ_DROPON_OFFSET%@$MJ_DROPON_OFFSET@g" /usr/local/nginx/conf/modjpeg.conf 34 | sed -i"" "s@%MJ_DROPON_FILE%@$MJ_DROPON_FILE@g" /usr/local/nginx/conf/modjpeg.conf 35 | 36 | cat /usr/local/nginx/conf/modjpeg.conf 37 | 38 | exec /usr/local/nginx/sbin/nginx -p /usr/local/nginx -c conf/modjpeg.conf 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Ingo Oppermann 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest AS build 2 | 3 | ENV \ 4 | NGINX_VERSION=1.26.2 \ 5 | LIBMODJPEG_VERSION=1.0.2 6 | 7 | RUN apk add --update \ 8 | binutils \ 9 | wget \ 10 | coreutils \ 11 | gcc \ 12 | g++ \ 13 | make \ 14 | cmake \ 15 | pcre-dev \ 16 | zlib-dev \ 17 | jpeg-dev \ 18 | libpng-dev 19 | 20 | RUN \ 21 | mkdir /dist && cd /dist && \ 22 | wget "https://github.com/ioppermann/libmodjpeg/archive/v${LIBMODJPEG_VERSION}.tar.gz" && \ 23 | tar -xzvf "v${LIBMODJPEG_VERSION}.tar.gz" && \ 24 | cd libmodjpeg-${LIBMODJPEG_VERSION} && \ 25 | cmake . && \ 26 | make -j$(nproc) && \ 27 | make install && \ 28 | rm -rf ${DIR} 29 | 30 | ADD config /dist/modjpeg-nginx/config 31 | ADD ngx_http_jpeg_filter_module.c /dist/modjpeg-nginx/ngx_http_jpeg_filter_module.c 32 | 33 | RUN \ 34 | cd /dist && \ 35 | wget "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" && \ 36 | tar -xzvf "nginx-${NGINX_VERSION}.tar.gz" && \ 37 | cd nginx-${NGINX_VERSION} && \ 38 | ./configure --prefix=/usr/local/nginx --add-module=/dist/modjpeg-nginx && \ 39 | make -j$(nproc) && \ 40 | make install && \ 41 | rm -rf ${DIR} 42 | 43 | FROM alpine:latest 44 | 45 | COPY --from=build /usr/local/nginx /usr/local/nginx 46 | COPY --from=build /usr/local/lib /usr/local/lib 47 | COPY --from=build /usr/lib /usr/lib 48 | 49 | ADD contrib/modjpeg.conf.in /usr/local/nginx/conf/modjpeg.conf.in 50 | ADD contrib/dropon.png /usr/local/nginx/conf/dropon.png 51 | ADD contrib/run.sh /usr/local/nginx/bin/run.sh 52 | 53 | EXPOSE 80/tcp 54 | VOLUME ["/images"] 55 | 56 | CMD ["/usr/local/nginx/bin/run.sh"] 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # modjpeg-nginx 2 | 3 | Nginx filter module for adding overlays on JPEGs on-the-fly with [libmodjpeg](https://github.com/ioppermann/libmodjpeg). 4 | 5 | > With libmodjpeg you can overlay a (masked) image onto an existing JPEG as lossless as possible. Changes in the JPEG only 6 | > take place where the overlayed image is applied. All modifications happen in the DCT domain, thus the JPEG is decoded and 7 | > encoded losslessly. 8 | 9 | - [Typical Uses](#typical-uses) 10 | - [Try it out](#try-it-out) 11 | - [Installation](#installation) 12 | - [Compatibility](#compatibility) 13 | - [Synopsis](#synopsis) 14 | - [Directives](#directives) 15 | - [jpeg_filter](#jpeg_filter) 16 | - [jpeg_filter_max_pixel](#jpeg_filter_max_pixel) 17 | - [jpeg_filter_buffer](#jpeg_filter_buffer) 18 | - [jpeg_filter_optimize](#jpeg_filter_optimize) 19 | - [jpeg_filter_progressive](#jpeg_filter_progressive) 20 | - [jpeg_filter_arithmetric](#jpeg_filter_arithmetric) 21 | - [jpeg_filter_graceful](#jpeg_filter_graceful) 22 | - [jpeg_filter_effect](#jpeg_filter_effect) 23 | - [jpeg_filter_dropon_align](#jpeg_filter_dropon_align) 24 | - [jpeg_filter_dropon_offset](#jpeg_filter_dropon_offset) 25 | - [jpeg_filter_dropon_file](#jpeg_filter_dropon_file) 26 | - [jpeg_filter_dropon_memory](#jpeg_filter_dropon_memory) 27 | - [Notes](#notes) 28 | - [License](#license) 29 | - [Acknowledgement](#acknowledgement) 30 | 31 | ## Typical Uses 32 | 33 | This filter module can add overlays (e.g. a logo, visual watermark) on JPEGs when they are requested. 34 | 35 | A few ideas: 36 | 37 | - Consider you are a photographer and have a image gallery on your website. Without hardcoding your logo (brand, watermark, ...) into these images you can apply it the moment the image is requested. Whenever you update your logo, just update the nginx configuration and it's done. No need to re-process all your images. 38 | - You have an online shop with thousands of product images. With just configuring nginx you can add your logo to all of the product images. You don't have to process all product images. 39 | - You have a paid service. Add a watermark to all images if the user is not subscribed. If the user is subscribed, don't apply the watermark or put just a small logo on the images without touching the original images. 40 | - On your website, registered users can upload images. Add the avatar of the user to the image who uploaded the image without processing it after the upload. If the user changes her avatar, all her images will automatically have the new avatar on them. 41 | 42 | ## Try it out 43 | 44 | In order to try out this filter module, pull the docker image 45 | 46 | ```bash 47 | docker pull ioppermann/modjpeg-nginx:latest 48 | ``` 49 | 50 | The docker container exposes TCP port 80 and expects a directory with images mounted on `/images`, e.g. 51 | 52 | ```bash 53 | docker run -it --rm --name=modjpeg-nginx \ 54 | --mount type=bind,src=$PWD/images,dst=/images,readonly \ 55 | -p 8080:80 \ 56 | ioppermann/modjpeg-nginx:latest 57 | ``` 58 | 59 | Now you can browse to [http://localhost:8080/](http://localhost:8080/) and click on the listed images. The modjpeg logo will be applied in the top left corner. By default 60 | only images that are smaller than 10MB are processed by the filter. Stop the container by pressing `Ctrl-c`. 61 | 62 | The filter can be controlled by these environment variables: 63 | 64 | | Name | Default | Description | 65 | | ---------------- | ---------------------------------- | ----------------------------------------------------------- | 66 | | MJ_GRACEFUL | on | See [jpeg_filter_graceful](#jpeg_filter_graceful) | 67 | | MJ_BUFFER | 10M | See [jpeg_filter_buffer](#jpeg_filter_buffer) | 68 | | MJ_MAX_PIXEL | 0 | See [jpeg_filter_max_pixel](#jpeg_filter_max_pixel) | 69 | | MJ_DROPON_ALIGN | "top left" | See [jpeg_filter_dropon_align](#jpeg_filter_dropon_align) | 70 | | MJ_DROPON_OFFSET | "0 0" | See [jpeg_filter_dropon_offset](#jpeg_filter_dropon_offset) | 71 | | MJ_DROPON_FILE | "/usr/local/nginx/conf/dropon.png" | See [jpeg_filter_dropon_file](#jpeg_filter_dropon_file) | 72 | 73 | The following example will allow images with up to 150 megapixel (`MJ_MAX_PIXEL`) and 100MB in file size (`MJ_BUFFER`). The logo will be placed in bottom right corner (`MJ_DROPON_ALIGN`) 74 | with an offset of -15px horizontally and vertically (`MJ_DROPON_OFFSET`). 75 | 76 | ```bash 77 | docker run -it --rm --name=modjpeg-nginx \ 78 | --mount type=bind,src=$PWD/images,dst=/images,readonly \ 79 | -p 8080:80 \ 80 | -e MJ_MAX_PIXEL=150000000 \ 81 | -e MJ_BUFFER=100M \ 82 | -e MJ_DROPON_ALIGN="bottom right" \ 83 | -e MJ_DROPON_OFFSET="-15 -15" \ 84 | ioppermann/modjpeg-nginx:latest 85 | ``` 86 | 87 | In order to change the logo, you can mount an additional volume or put it into the directory you already mount, e.g. 88 | 89 | ```bash 90 | docker run -it --rm --name=modjpeg-nginx \ 91 | --mount type=bind,src=$PWD/images,dst=/images,readonly \ 92 | -p 8080:80 \ 93 | -e MJ_DROPON_FILE="/images/logo.png" \ 94 | ioppermann/modjpeg-nginx:latest 95 | ``` 96 | 97 | ## Installation 98 | 99 | ### CentOS / RedHat 7 packages 100 | 101 | An easy way to use the module in CentOS or RedHat 7, is to use precompiled dynamic nginx module. It is built for nginx stable. The [repository](https://www.getpagespeed.com/redhat) includes latest stable nginx, the jpeg module, libmodjpeg and libpng16 dependencies: 102 | 103 | yum -y install https://extras.getpagespeed.com/release-el7-latest.rpm 104 | yum install nginx nginx-module-jpeg 105 | 106 | ### Compilation 107 | 108 | For using the modjpeg-nginx filter module, follow these steps: 109 | 110 | 1. Clone and install [libmodjpeg](https://github.com/ioppermann/libmodjpeg) (libjpeg and cmake are required) 111 | 2. Clone this repository 112 | 3. Download and extract the [latest nginx](http://nginx.org/en/download.html) 113 | 4. Configure, compile, and install nginx 114 | 115 | ```bash 116 | # Clone and install libmodjpeg 117 | git clone https://github.com/ioppermann/libmodjpeg.git 118 | cd libmodjpeg 119 | cmake . 120 | make 121 | make install 122 | cd .. 123 | 124 | # Clone modjpeg-nginx 125 | git clone https://github.com/ioppermann/modjpeg-nginx.git 126 | 127 | # Download and install nginx 128 | wget 'http://nginx.org/download/nginx-1.19.2.tar.gz' 129 | tar -xvzf nginx-1.19.2.tar.gz 130 | cd nginx-1.19.2 131 | 132 | # Configure as static module, or ... 133 | ./configure --add_module=../modjpeg-nginx 134 | 135 | # ... configure as dynamic module (as of nginx 1.9.11) 136 | ./configure --add_dynamic_module=../modjpeg-nginx 137 | 138 | # If the libmodjpeg library is not found, add e.g. '--with-ld-opt=-L/usr/local/lib' to 139 | # the configure options if it was installed to /usr/local/lib 140 | 141 | # You may want to use the other './configure' options that are used 142 | # in your current nginx build. Check the output of 'nginx -V'. 143 | 144 | make 145 | make install 146 | ``` 147 | 148 | If you configured modjpeg-nginx as dynamic module, you have to load the module in the beginning of the config 149 | 150 | ```nginx 151 | ... 152 | load_module modules/ngx_http_jpeg_filter_module.so; 153 | ... 154 | ``` 155 | 156 | ## Compatibility 157 | 158 | This module has been tested with the following versions of nginx: 159 | 160 | - 1.26.2 161 | - 1.24.0 162 | - 1.22.1 163 | - 1.20.2 164 | - 1.19.2 165 | - 1.18.0 166 | - 1.17.6 167 | - 1.16.1 168 | - 1.15.3 169 | - 1.14.0 170 | - 1.13.10 171 | - 1.12.2 172 | - 1.10.3 173 | - 1.8.1 (only as static module) 174 | 175 | ## Synopsis 176 | 177 | ```nginx 178 | ... 179 | 180 | location /gallery { 181 | # enable jpeg filter module 182 | jpeg_filter on; 183 | 184 | # limit image sizes to 9 megapixel 185 | jpeg_filter_max_pixel 9000000; 186 | 187 | # limit image file size to 5 megabytes 188 | jpeg_filter_buffer 5M; 189 | 190 | # deliver the images unmodified if one of the limits apply 191 | jpeg_filter_graceful on; 192 | 193 | # pixelate the image 194 | jpeg_filter_effect pixelate; 195 | 196 | # add a masked logo in the bottom right corner 197 | # with a distance of 10 pixel from the border 198 | jpeg_filter_dropon_align bottom right; 199 | jpeg_filter_dropon_offset -10 -10; 200 | jpeg_filter_dropon_file /path/to/logo.jpg /path/to/mask.jpg; 201 | } 202 | 203 | ... 204 | ``` 205 | 206 | Or use it with [OpenResty's ngx_http_lua_module](https://github.com/openresty/lua-nginx-module) and a PNG logo: 207 | 208 | ```nginx 209 | ... 210 | 211 | location /gallery { 212 | set_by_lua_block $valign { 213 | local a = { 'top', 'center', 'bottom' } 214 | return a[math.random(#a)] 215 | } 216 | 217 | set_by_lua_block $halign { 218 | local a = { 'left', 'center', 'right' } 219 | return a[math.random(#a)] 220 | } 221 | 222 | # enable jpeg filter module 223 | jpeg_filter on; 224 | 225 | # limit image sizes to 9 megapixel 226 | jpeg_filter_max_pixel 9000000; 227 | 228 | # limit image file size to 5 megabytes 229 | jpeg_filter_buffer 5M; 230 | 231 | # deliver the images unmodified if one of the limits apply 232 | jpeg_filter_graceful on; 233 | 234 | # pixelate the image 235 | jpeg_filter_effect pixelate; 236 | 237 | # add a logo in a random position 238 | jpeg_filter_dropon_align $valign $halign; 239 | jpeg_filter_dropon_file /path/to/logo.png; 240 | } 241 | 242 | ... 243 | ``` 244 | 245 | Or generate a logo with [Lua-GD](http://ittner.github.io/lua-gd/): 246 | 247 | ```nginx 248 | http { 249 | ... 250 | lua_package_cpath '/usr/local/lib/lua/5.1/?.so;;'; 251 | ... 252 | server { 253 | ... 254 | location /gallery { 255 | set_by_lua_block $logobytestream { 256 | local gd = require "gd" 257 | 258 | local im = gd.create(210, 70) 259 | local white = im:colorAllocate(255, 255, 255) 260 | local black = im:colorAllocate(0, 0, 0) 261 | im:filledRectangle(0, 0, 140, 80, white) 262 | im:string(gd.FONT_LARGE, 10, 10, "Hello modjpeg", black) 263 | im:string(gd.FONT_LARGE, 10, 40, os.date("%c"), black); 264 | return im:jpegStr(85) 265 | } 266 | 267 | # enable jpeg filter module 268 | jpeg_filter on; 269 | 270 | # limit image sizes to 9 megapixel 271 | jpeg_filter_max_pixel 9000000; 272 | 273 | # limit image file size to 5 megabytes 274 | jpeg_filter_buffer 5M; 275 | 276 | # deliver the images unmodified if one of the limits apply 277 | jpeg_filter_graceful on; 278 | 279 | # pixelate the image 280 | jpeg_filter_effect pixelate; 281 | 282 | # add a generated logo in the bottom right corner 283 | # with a distance of 10 pixel from the border 284 | jpeg_filter_dropon_align bottom right; 285 | jpeg_filter_dropon_offset -10 -10; 286 | jpeg_filter_dropon_memory $logobytestream; 287 | } 288 | ... 289 | } 290 | ... 291 | } 292 | ``` 293 | 294 | ## Directives 295 | 296 | - [jpeg_filter](#jpeg_filter) 297 | - [jpeg_filter_max_pixel](#jpeg_filter_max_pixel) 298 | - [jpeg_filter_buffer](#jpeg_filter_buffer) 299 | - [jpeg_filter_optimize](#jpeg_filter_optimize) 300 | - [jpeg_filter_progressive](#jpeg_filter_progressive) 301 | - [jpeg_filter_arithmetric](#jpeg_filter_arithmetric) 302 | - [jpeg_filter_graceful](#jpeg_filter_graceful) 303 | - [jpeg_filter_effect](#jpeg_filter_effect) 304 | - [jpeg_filter_dropon_align](#jpeg_filter_dropon_align) 305 | - [jpeg_filter_dropon_offset](#jpeg_filter_dropon_offset) 306 | - [jpeg_filter_dropon_file](#jpeg_filter_dropon_file) 307 | - [jpeg_filter_dropon_memory](#jpeg_filter_dropon_memory) 308 | - [Notes](#notes) 309 | 310 | ### jpeg_filter 311 | 312 | **Syntax:** `jpeg_filter on | off` 313 | 314 | **Default:** `jpeg_filter off` 315 | 316 | **Context:** `location` 317 | 318 | Enable the jpeg filter module. 319 | 320 | This directive is turned off by default. 321 | 322 | ### jpeg_filter_max_pixel 323 | 324 | **Syntax:** `jpeg_filter_max_pixel pixel` 325 | 326 | **Default:** `0` 327 | 328 | **Context:** `http, server, location` 329 | 330 | Maximum number of pixel in image to operate on. If the image has more pixel (width \* height) than `pixel`, the jpeg filter will return a "415 Unsupported Media Type". 331 | Set [jpeg_filter_graceful](#jpeg_filter_graceful) to `on` to deliver the image unchanged. Set the maximum pixel to 0 in order ignore the image dimensions. 332 | 333 | This directive is set to 0 by default. 334 | 335 | ### jpeg_filter_buffer 336 | 337 | **Syntax:** `jpeg_filter_buffer size` 338 | 339 | **Default:** `2M` 340 | 341 | **Context:** `http, server, location` 342 | 343 | The maximum file size of the image to operate on. If the file size if bigger than `size`, the jpeg filter will return a "415 Unsupported Media Type". 344 | Set [jpeg_filter_graceful](#jpeg_filter_graceful) to `on` to deliver the image unchanged. 345 | 346 | This directive is set to 2 megabyte by default. 347 | 348 | ### jpeg_filter_optimize 349 | 350 | **Syntax:** `jpeg_filter_optimize on | off` 351 | 352 | **Default:** `off` 353 | 354 | **Context:** `http, server, location` 355 | 356 | Upon delivery, optimize the Huffman tables of the image. 357 | 358 | This directive is turned off by default. 359 | 360 | ### jpeg_filter_progressive 361 | 362 | **Syntax:** `jpeg_filter_progressive on | off` 363 | 364 | **Default:** `off` 365 | 366 | **Context:** `http, server, location` 367 | 368 | Upon delivery, enable progressive encoding of the image. 369 | 370 | This directive is turned off by default. 371 | 372 | ### jpeg_filter_arithmetric 373 | 374 | **Syntax:** `jpeg_filter_arithmetric on | off` 375 | 376 | **Default:** `off` 377 | 378 | **Context:** `http, server, location` 379 | 380 | Upon delivery, enable arithmetric encoding of the image. 381 | This will override the [jpeg_filter_optimize](#jpeg_filter_optimize) directive. 382 | Arithmetric encoding is usually not supported by browsers. 383 | 384 | This directive is turned off by default. 385 | 386 | ### jpeg_filter_graceful 387 | 388 | **Syntax:** `jpeg_filter_graceful on | off` 389 | 390 | **Default:** `off` 391 | 392 | **Context:** `http, server, location` 393 | 394 | Allow to deliver the unchanged image in case the directives [jpeg_filter_max_pixel](#jpeg_filter_max_pixel) or [jpeg_filter_buffer](#jpeg_filter_buffer) would return a "415 Unsupported Media Type" error. 395 | 396 | This directive is turned off by default. 397 | 398 | ### jpeg_filter_effect 399 | 400 | **Syntax:** `jpeg_filter_effect grayscale | pixelate` 401 | 402 | **Syntax:** `jpeg_filter_effect darken | brighten value` 403 | 404 | **Syntax:** `jpeg_filter_effect tintblue | tintyellow | tintred | tintgreen value` 405 | 406 | **Default:** `-` 407 | 408 | **Context:** `location` 409 | 410 | Apply an effect to the image. 411 | 412 | `grayscale` will remove all color components from the image. This only applies to images in the YCbCr color space. 413 | 414 | `pixelate` will pixelate the image in blocks of 8x8 pixel by setting the AC coefficients in all components to 0. 415 | 416 | `darken` will darken the image by decreasing the DC coefficients in the Y component by `value`. This only applies to images in the YCbCr color space. 417 | 418 | `brighten` will brighten the image by increasing the DC coefficients in the Y component by `value`. This only applies to images in the YCbCr color space. 419 | 420 | `tintblue` will tint the image blue by increasing the DC coefficients in the Cb component by `value`. This only applies to images in the YCbCr color space. 421 | 422 | `tintyellow` will tint the image blue by decreasing the DC coefficients in the Cb component by `value`. This only applies to images in the YCbCr color space. 423 | 424 | `tintred` will tint the image red by increasing the DC coefficients in the Cr component by `value`. This only applies to images in the YCbCr color space. 425 | 426 | `tintgreen` will tint the image green by decreasing the DC coefficients in the Cr component by `value`. This only applies to images in the YCbCr color space. 427 | 428 | This directive is not set by default. 429 | 430 | All parameters can contain variables. 431 | 432 | ### jpeg_filter_dropon_align 433 | 434 | **Syntax:** `jpeg_filter_dropon_align [top | center | bottom] [left | center | right]` 435 | 436 | **Default:** `center center` 437 | 438 | **Context:** `location` 439 | 440 | Align the dropon on the image. Use the directive [jpeg_filter_dropon_offset](#jpeg_filter_dropon_offset) to offset the dropon from the alignment. 441 | 442 | This directive must be set before [jpeg_filter_dropon](#jpeg_filter_dropon) in order to have an effect on the dropon. 443 | 444 | This directive will apply the dropon in the center of the image by default. 445 | 446 | All parameters can contain variables. 447 | 448 | ### jpeg_filter_dropon_offset 449 | 450 | **Syntax:** `jpeg_filter_dropon_offset vertical horizontal` 451 | 452 | **Default:** `0 0` 453 | 454 | **Context:** `location` 455 | 456 | Offset the dropon by `vertical` and `horizontal` pixels from the alignment given with the [jpeg_filter_dropon_align](#jpeg_filter_dropon_align) directive. 457 | Use a negative value to move the dropon up or left and a positive value to move the dropon down or right. 458 | 459 | This directive must be set before [jpeg_filter_dropon](#jpeg_filter_dropon) in order to have an effect on the dropon. 460 | 461 | This directive will not apply an offset by default. 462 | 463 | All parameters can contain variables. 464 | 465 | ### jpeg_filter_dropon_file 466 | 467 | **Syntax:** `jpeg_filter_dropon_file image` 468 | 469 | **Syntax:** `jpeg_filter_dropon_file image mask` 470 | 471 | **Default:** `-` 472 | 473 | **Context:** `location` 474 | 475 | Apply a dropon to the image. The dropon is given by a path to a JPEG or PNG image for `image` and optionally a path to a JPEG image for `mask`. If no mask image is 476 | provided, the image will be applied without transcluency. If a mask image is provided, only the luminance component will be used. For the mask, black means 477 | fully transcluent and white means fully opaque. Any values inbetween will blend the underlying image and the dropon accordingly. If `image` is a path to a PNG, the 478 | mask will be ignored. 479 | 480 | This directive is not set by default. 481 | 482 | All parameters can contain variables. 483 | 484 | If none of the parameters contain variables, the dropon is loaded during loading of the configuration. If at least one parameter contains variables, the dropon 485 | will be loaded during processing of the request. After processing the request, the dropon will be unloaded. 486 | 487 | PNG files as dropon are supported only if libmodjpeg has been compiled with PNG support. 488 | 489 | ### jpeg_filter_dropon_memory 490 | 491 | **Syntax:** `jpeg_filter_dropon_memory $image` 492 | 493 | **Syntax:** `jpeg_filter_dropon_memory $image $mask` 494 | 495 | **Default:** `-` 496 | 497 | **Context:** `location` 498 | 499 | Apply a dropon to the image. The dropon is given by a variable holding a JPEG or PNG image bytestream for `$image` and optionally a variable to a JPEG image bytestream for `$mask`. 500 | If no mask image is provided, the image will be applied without transcluency. If a mask image is provided, only the luminance component will be used. For the mask, 501 | black means fully transcluent and white means fully opaque. Any values inbetween will blend the underlying image and the dropon accordingly. If `$image` is a PNG, the 502 | mask will be ignored. 503 | 504 | This directive is not set by default. 505 | 506 | All parameters are expected to be variables. 507 | 508 | The dropon will always be loaded during processing of the request. After processing the request, the dropon will be unloaded. 509 | 510 | PNG bytestreams as dropon are supported only if libmodjpeg has been compiled with PNG support. 511 | 512 | ### Notes 513 | 514 | The directives `jpeg_filter_effect`, `jpeg_filter_dropon_align`, `jpeg_filter_dropon_offset`, and `jpeg_filter_dropon` are applied in the order they 515 | appear in the nginx config file, i.e. it makes a difference if you apply first an effect and then add a dropon or vice versa. In the former case the dropon will be 516 | unaffected by the effect and in the latter case the effect will be also applied on the dropon. 517 | 518 | ## License 519 | 520 | This module is distributed under the BSD license. Refer to [LICENSE](/blob/master/LICENSE). 521 | 522 | ## Acknowledgement 523 | 524 | This module is heavily inspired by the nginx image filter module with 525 | insights from 526 | ["Emiller’s Guide To Nginx Module Development"](https://www.evanmiller.org/nginx-modules-guide.html) 527 | and the 528 | [nginx development guide](https://nginx.org/en/docs/dev/development_guide.html). 529 | -------------------------------------------------------------------------------- /ngx_http_jpeg_filter_module.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Ingo Oppermann 3 | * 4 | * JPEG filter module using libmodjpeg (github.com/ioppermann/libmodjpeg) 5 | * 6 | * This module is heavily inspired by the image filter module with 7 | * insights from 8 | * 9 | * "Emiller’s Guide To Nginx Module Development" 10 | * https://www.evanmiller.org/nginx-modules-guide.html 11 | * 12 | * and the 13 | * 14 | * nginx development guide 15 | * https://nginx.org/en/docs/dev/development_guide.html. 16 | * 17 | * Directives: 18 | * 19 | * jpeg_filter on|off 20 | * Default: off 21 | * Context: location 22 | * 23 | * jpeg_filter_max_pixel pixel 24 | * Default: 0 25 | * Context: http, server, location 26 | * 27 | * jpeg_filter_optimize on|off 28 | * Default: off 29 | * Context: http, server, location 30 | * 31 | * jpeg_filter_progressive on|off 32 | * Default: off 33 | * Context: http, server, location 34 | * 35 | * jpeg_filter_arithmetric on|off 36 | * Default: off 37 | * Context: http, server, location 38 | * 39 | * jpeg_filter_graceful on|off 40 | * Default: off 41 | * Context: http, server, location 42 | * 43 | * jpeg_filter_buffer size 44 | * Default: 2M 45 | * Context: http, server, location 46 | * 47 | * jpeg_filter_effect grayscale|pixelate 48 | * jpeg_filter_effect darken|brighten value 49 | * jpeg_filter_effect tintblue|tintyellow|tintred|tintgreen value 50 | * Default: - 51 | * Context: location 52 | * 53 | * jpeg_filter_dropon_align top|center|bottom left|center|right 54 | * Default: center center 55 | * Context: location 56 | * 57 | * jpeg_filter_dropon_offset vertical horizontal 58 | * Default: 0 0 59 | * Context: location 60 | * 61 | * jpeg_filter_dropon_file image 62 | * jpeg_filter_dropon_file image mask 63 | * Default: - 64 | * Context: location 65 | * 66 | * jpeg_filter_dropon_memory image 67 | * jpeg_filter_dropon_memory image mask 68 | * Default: - 69 | * Context: location 70 | * 71 | */ 72 | 73 | #include 74 | #include 75 | #include 76 | 77 | #include 78 | 79 | #define NGX_HTTP_IMAGE_NONE 0 80 | #define NGX_HTTP_IMAGE_JPEG 1 81 | 82 | #define NGX_HTTP_IMAGE_BUFFERED 0x08 83 | 84 | /* Phases of the body filter */ 85 | #define NGX_HTTP_JPEG_FILTER_PHASE_START 0 86 | #define NGX_HTTP_JPEG_FILTER_PHASE_READ 1 87 | #define NGX_HTTP_JPEG_FILTER_PHASE_PROCESS 2 88 | #define NGX_HTTP_JPEG_FILTER_PHASE_PASS 3 89 | #define NGX_HTTP_JPEG_FILTER_PHASE_DONE 4 90 | 91 | #define NGX_HTTP_JPEG_FILTER_UNMODIFIED 0 92 | #define NGX_HTTP_JPEG_FILTER_MODIFIED 1 93 | 94 | /* Types for the filter elements */ 95 | #define NGX_HTTP_JPEG_FILTER_TYPE_EFFECT1 1 96 | #define NGX_HTTP_JPEG_FILTER_TYPE_EFFECT2 2 97 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_ALIGN 3 98 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_OFFSET 4 99 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON 5 100 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE1 6 101 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE2 7 102 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY1 8 103 | #define NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY2 9 104 | 105 | #define NGX_HTTP_JPEG_FILTER_BUFFER_SIZE 2 * 1024 * 1024 106 | 107 | /* Configuration of the elements in the processing chain */ 108 | typedef struct { 109 | ngx_uint_t type; /* Type of filter element */ 110 | ngx_http_complex_value_t cv1; /* First complex value. Depends on the type if it is used */ 111 | ngx_http_complex_value_t cv2; /* Second complex value. Depends on the type if it is used */ 112 | mj_dropon_t *dropon; /* libmodjpeg dropon type. Depends on the type if it is used */ 113 | } ngx_http_jpeg_filter_element_t; 114 | 115 | typedef struct { 116 | ngx_uint_t max_pixel; /* Max. allowed pixel in image */ 117 | 118 | ngx_flag_t enable; /* Whether the module is enabled */ 119 | ngx_flag_t optimize; /* Whether to optimize the Huffman tables in the resulting JPEG */ 120 | ngx_flag_t progressive; /* Whether the resulting JPEG should stored in progressive mode */ 121 | ngx_flag_t arithmetric; /* Whether to use arithmetric coding in the resulting JPEG */ 122 | ngx_flag_t graceful; /* Whether the unmodified image should be sent if processing fails */ 123 | 124 | ngx_array_t *filter_elements; /* Processing chain */ 125 | 126 | size_t buffer_size; /* Max. allowed size of the body */ 127 | } ngx_http_jpeg_filter_conf_t; 128 | 129 | typedef struct { 130 | u_char *in_image; /* Holds the original image */ 131 | u_char *in_last; /* Pointer to the end of in_image */ 132 | 133 | u_char *out_image; /* Holds the final processed image */ 134 | u_char *out_last; /* Pointer to the end of out_image */ 135 | 136 | size_t length; /* Size of the original image */ 137 | 138 | ngx_uint_t width; /* Width of the original image */ 139 | ngx_uint_t height; /* Height of the original image */ 140 | 141 | ngx_uint_t phase; /* The current phase the module is in */ 142 | ngx_uint_t skip; /* Skip the processing of the body */ 143 | } ngx_http_jpeg_filter_ctx_t; 144 | 145 | /* The filter functions */ 146 | static ngx_int_t ngx_http_jpeg_header_filter(ngx_http_request_t *r); 147 | static ngx_int_t ngx_http_jpeg_body_filter(ngx_http_request_t *r, ngx_chain_t *in); 148 | 149 | /* Helper for the filter functions */ 150 | static ngx_int_t ngx_http_jpeg_filter_send(ngx_http_request_t *r, ngx_uint_t image); 151 | static ngx_uint_t ngx_http_jpeg_filter_test(ngx_http_request_t *r, ngx_chain_t *in); 152 | static ngx_int_t ngx_http_jpeg_filter_read(ngx_http_request_t *r, ngx_chain_t *in); 153 | static ngx_int_t ngx_http_jpeg_filter_process(ngx_http_request_t *r); 154 | static void ngx_http_jpeg_filter_cleanup(void *data); 155 | 156 | /* Handling the configuration directives for the effects and dropon */ 157 | static char *ngx_conf_jpeg_filter_effect(ngx_conf_t *cf, ngx_command_t *cmd, void *c); 158 | static char *ngx_conf_jpeg_filter_dropon(ngx_conf_t *cf, ngx_command_t *cmd, void *c); 159 | 160 | /* Configuration functions */ 161 | static void *ngx_http_jpeg_filter_create_conf(ngx_conf_t *cf); 162 | static char *ngx_http_jpeg_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child); 163 | static ngx_int_t ngx_http_jpeg_filter_init(ngx_conf_t *cf); 164 | static void ngx_http_jpeg_filter_conf_cleanup(void *data); 165 | 166 | /* Helper functions for complex values */ 167 | static ngx_int_t ngx_http_jpeg_filter_get_int_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_int_t defval); 168 | static ngx_int_t ngx_http_jpeg_filter_get_string_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_str_t *val); 169 | 170 | /* Configuration directives */ 171 | static ngx_command_t ngx_http_jpeg_filter_commands[] = { 172 | { ngx_string("jpeg_filter"), 173 | NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 174 | ngx_conf_set_flag_slot, 175 | NGX_HTTP_LOC_CONF_OFFSET, 176 | offsetof(ngx_http_jpeg_filter_conf_t, enable), 177 | NULL }, 178 | 179 | { ngx_string("jpeg_filter_max_pixel"), 180 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 181 | ngx_conf_set_num_slot, 182 | NGX_HTTP_LOC_CONF_OFFSET, 183 | offsetof(ngx_http_jpeg_filter_conf_t, max_pixel), 184 | NULL }, 185 | 186 | { ngx_string("jpeg_filter_optimize"), 187 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 188 | ngx_conf_set_flag_slot, 189 | NGX_HTTP_LOC_CONF_OFFSET, 190 | offsetof(ngx_http_jpeg_filter_conf_t, optimize), 191 | NULL }, 192 | 193 | { ngx_string("jpeg_filter_progressive"), 194 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 195 | ngx_conf_set_flag_slot, 196 | NGX_HTTP_LOC_CONF_OFFSET, 197 | offsetof(ngx_http_jpeg_filter_conf_t, progressive), 198 | NULL }, 199 | 200 | { ngx_string("jpeg_filter_arithmetric"), 201 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 202 | ngx_conf_set_flag_slot, 203 | NGX_HTTP_LOC_CONF_OFFSET, 204 | offsetof(ngx_http_jpeg_filter_conf_t, arithmetric), 205 | NULL }, 206 | 207 | { ngx_string("jpeg_filter_graceful"), 208 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, 209 | ngx_conf_set_flag_slot, 210 | NGX_HTTP_LOC_CONF_OFFSET, 211 | offsetof(ngx_http_jpeg_filter_conf_t, graceful), 212 | NULL }, 213 | 214 | { ngx_string("jpeg_filter_buffer"), 215 | NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, 216 | ngx_conf_set_size_slot, 217 | NGX_HTTP_LOC_CONF_OFFSET, 218 | offsetof(ngx_http_jpeg_filter_conf_t, buffer_size), 219 | NULL }, 220 | 221 | { ngx_string("jpeg_filter_effect"), 222 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12, 223 | ngx_conf_jpeg_filter_effect, 224 | NGX_HTTP_LOC_CONF_OFFSET, 225 | 0, 226 | NULL }, 227 | 228 | { ngx_string("jpeg_filter_dropon_align"), 229 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, 230 | ngx_conf_jpeg_filter_dropon, 231 | NGX_HTTP_LOC_CONF_OFFSET, 232 | 0, 233 | NULL }, 234 | 235 | { ngx_string("jpeg_filter_dropon_offset"), 236 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE2, 237 | ngx_conf_jpeg_filter_dropon, 238 | NGX_HTTP_LOC_CONF_OFFSET, 239 | 0, 240 | NULL }, 241 | 242 | { ngx_string("jpeg_filter_dropon_file"), 243 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12, 244 | ngx_conf_jpeg_filter_dropon, 245 | NGX_HTTP_LOC_CONF_OFFSET, 246 | 0, 247 | NULL }, 248 | 249 | { ngx_string("jpeg_filter_dropon_memory"), 250 | NGX_HTTP_LOC_CONF|NGX_CONF_TAKE12, 251 | ngx_conf_jpeg_filter_dropon, 252 | NGX_HTTP_LOC_CONF_OFFSET, 253 | 0, 254 | NULL }, 255 | 256 | ngx_null_command 257 | }; 258 | 259 | static ngx_http_module_t ngx_http_jpeg_filter_module_ctx = { 260 | NULL, /* preconfiguration */ 261 | ngx_http_jpeg_filter_init, /* postconfiguration */ 262 | 263 | NULL, /* create main configuration */ 264 | NULL, /* init main configuration */ 265 | 266 | NULL, /* create server configuration */ 267 | NULL, /* merge server configuration */ 268 | 269 | ngx_http_jpeg_filter_create_conf, /* create location configuration */ 270 | ngx_http_jpeg_filter_merge_conf /* merge location configuration */ 271 | }; 272 | 273 | ngx_module_t ngx_http_jpeg_filter_module = { 274 | NGX_MODULE_V1, 275 | &ngx_http_jpeg_filter_module_ctx, /* module context */ 276 | ngx_http_jpeg_filter_commands, /* module directives */ 277 | NGX_HTTP_MODULE, /* module type */ 278 | NULL, /* init master */ 279 | NULL, /* init module */ 280 | NULL, /* init process */ 281 | NULL, /* init thread */ 282 | NULL, /* exit thread */ 283 | NULL, /* exit process */ 284 | NULL, /* exit master */ 285 | NGX_MODULE_V1_PADDING 286 | }; 287 | 288 | static ngx_http_output_header_filter_pt ngx_http_next_header_filter; 289 | static ngx_http_output_body_filter_pt ngx_http_next_body_filter; 290 | 291 | static ngx_int_t ngx_http_jpeg_header_filter(ngx_http_request_t *r) { 292 | off_t len; 293 | ngx_http_jpeg_filter_ctx_t *ctx; 294 | ngx_http_jpeg_filter_conf_t *conf; 295 | 296 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_header_filter"); 297 | 298 | if(r->headers_out.status == NGX_HTTP_NOT_MODIFIED) { 299 | return ngx_http_next_header_filter(r); 300 | } 301 | 302 | /* Check if we already have a context for this request and our module */ 303 | ctx = ngx_http_get_module_ctx(r, ngx_http_jpeg_filter_module); 304 | if(ctx) { 305 | /* There is already a context for this filter? Remove it, next! */ 306 | ngx_http_set_ctx(r, NULL, ngx_http_jpeg_filter_module); 307 | return ngx_http_next_header_filter(r); 308 | } 309 | 310 | /* Get our configuration */ 311 | conf = ngx_http_get_module_loc_conf(r, ngx_http_jpeg_filter_module); 312 | if(conf->enable == 0) { 313 | /* This filter is not enabled. Next! */ 314 | return ngx_http_next_header_filter(r); 315 | } 316 | 317 | /* Check for multipart/x-mixed-replace. We can't handle this. Next */ 318 | if( 319 | r->headers_out.content_type.len >= sizeof("multipart/x-mixed-replace") - 1 && 320 | ngx_strncasecmp( 321 | r->headers_out.content_type.data, 322 | (u_char *) "multipart/x-mixed-replace", 323 | sizeof("multipart/x-mixed-replace") - 1 324 | ) == 0 325 | ) { 326 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "jpeg_filter: multipart/x-mixed-replace response"); 327 | 328 | return NGX_ERROR; 329 | } 330 | 331 | /* Allocate space for our context struct, so can store some state */ 332 | ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_jpeg_filter_ctx_t)); 333 | if (ctx == NULL) { 334 | return NGX_ERROR; 335 | } 336 | 337 | /* Associate our context struct with the request and module context */ 338 | ngx_http_set_ctx(r, ctx, ngx_http_jpeg_filter_module); 339 | 340 | /* 341 | * Check for the body length and if we support this. We need to buffer 342 | * the whole body and we have an upper limit for how much memory we are 343 | * willing to allocate. 344 | */ 345 | len = r->headers_out.content_length_n; 346 | 347 | if(len != -1 && len > (off_t)conf->buffer_size) { 348 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "jpeg_filter: too big response: %O", len); 349 | 350 | if(conf->graceful == 1) { 351 | ctx->skip = 1; 352 | return ngx_http_next_header_filter(r); 353 | } 354 | 355 | return NGX_HTTP_UNSUPPORTED_MEDIA_TYPE; 356 | } 357 | 358 | /* 359 | * In our context for this request, set the length of the body. We need this later 360 | * in the body filter to allocate the memory for the buffer that we're going to buffer. 361 | */ 362 | if(len == -1) { 363 | ctx->length = conf->buffer_size; 364 | } else { 365 | ctx->length = (size_t)len; 366 | } 367 | 368 | /* Copied from image_filter. No exact clue what this is doing */ 369 | if(r->headers_out.refresh) { 370 | r->headers_out.refresh->hash = 0; 371 | } 372 | 373 | /* Copied from image_filter. No clue what these are doing */ 374 | r->main_filter_need_in_memory = 1; 375 | r->allow_ranges = 0; 376 | 377 | /* 378 | * Do not call the next header filter because we don't know yet 379 | * the length of the modified body or if we like the original body. 380 | */ 381 | return NGX_OK; 382 | } 383 | 384 | static ngx_int_t ngx_http_jpeg_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { 385 | ngx_int_t rc; 386 | ngx_http_jpeg_filter_ctx_t *ctx; 387 | ngx_http_jpeg_filter_conf_t *conf; 388 | 389 | /* Now it's our turn */ 390 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_body_filter"); 391 | 392 | /* Bail out to the next body filter if there's no data */ 393 | if(in == NULL) { 394 | return ngx_http_next_body_filter(r, in); 395 | } 396 | 397 | /* Get the configuration for our filter */ 398 | conf = ngx_http_get_module_loc_conf(r, ngx_http_jpeg_filter_module); 399 | if(conf->enable == 0) { 400 | /* Our filter is not enabled. Next! */ 401 | return ngx_http_next_body_filter(r, in); 402 | } 403 | 404 | /* Get our context for this request that we allocated in the header filter */ 405 | ctx = ngx_http_get_module_ctx(r, ngx_http_jpeg_filter_module); 406 | if(ctx == NULL) { 407 | /* No context? Next! */ 408 | return ngx_http_next_body_filter(r, in); 409 | } 410 | 411 | if(ctx->skip == 1) { 412 | /* The header filter tells us to skip the processing of the body */ 413 | return ngx_http_next_body_filter(r, in); 414 | } 415 | 416 | /* 417 | * Because the body data it most probably split into several chains and this 418 | * function will be called more than once, we have to keep track in what "phase" we're in 419 | */ 420 | switch(ctx->phase) { 421 | case NGX_HTTP_JPEG_FILTER_PHASE_START: 422 | /* This is the first time we see some data for our filter */ 423 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: phase START"); 424 | 425 | /* 426 | * Have a taste of the first bytes of data in order to find out 427 | * if this actually something we should care about and can handle. 428 | */ 429 | if(ngx_http_jpeg_filter_test(r, in) == NGX_HTTP_IMAGE_NONE) { 430 | /* No image data. Send the header and pass on the data */ 431 | ctx->phase = NGX_HTTP_JPEG_FILTER_PHASE_PASS; 432 | 433 | /* Proceed to the next header filter as well because 434 | * we were holding it back so far. 435 | */ 436 | ngx_http_next_header_filter(r); 437 | return ngx_http_next_body_filter(r, in); 438 | } 439 | 440 | /* Following calls of this function go directly to the reading phase */ 441 | ctx->phase = NGX_HTTP_JPEG_FILTER_PHASE_READ; 442 | 443 | /* Fall through */ 444 | 445 | case NGX_HTTP_JPEG_FILTER_PHASE_READ: 446 | /* Here we want to read all the data into our buffer */ 447 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: phase READ"); 448 | 449 | rc = ngx_http_jpeg_filter_read(r, in); 450 | 451 | /* If there is more data, return nicely but don't call the next filter, so we will get more data! */ 452 | if(rc == NGX_AGAIN) { 453 | return NGX_OK; 454 | } 455 | 456 | /* If there was an error, abort and send some error code */ 457 | if(rc == NGX_ERROR) { 458 | return ngx_http_filter_finalize_request(r, &ngx_http_jpeg_filter_module, NGX_HTTP_INTERNAL_SERVER_ERROR); 459 | } 460 | 461 | /* fall through */ 462 | 463 | case NGX_HTTP_JPEG_FILTER_PHASE_PROCESS: 464 | /* Now that we have all the bytes from the image, we can go on an process it */ 465 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: phase PROCESS"); 466 | 467 | /* What ever comes after will be passed through */ 468 | ctx->phase = NGX_HTTP_JPEG_FILTER_PHASE_PASS; 469 | 470 | rc = ngx_http_jpeg_filter_process(r); 471 | if(rc == NGX_ERROR) { 472 | /* There was a problem processing the image. Either send the original image or an error */ 473 | 474 | if(conf->graceful == 1) { 475 | /* Send the original image */ 476 | return ngx_http_jpeg_filter_send(r, NGX_HTTP_JPEG_FILTER_UNMODIFIED); 477 | } 478 | else { 479 | return ngx_http_filter_finalize_request(r, &ngx_http_jpeg_filter_module, NGX_HTTP_UNSUPPORTED_MEDIA_TYPE); 480 | } 481 | } 482 | 483 | /* Send the modified image */ 484 | return ngx_http_jpeg_filter_send(r, NGX_HTTP_JPEG_FILTER_MODIFIED); 485 | 486 | case NGX_HTTP_JPEG_FILTER_PHASE_PASS: 487 | return ngx_http_next_body_filter(r, in); 488 | 489 | case NGX_HTTP_JPEG_FILTER_PHASE_DONE: 490 | default: 491 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: phase default (DONE)"); 492 | 493 | rc = ngx_http_next_body_filter(r, NULL); 494 | 495 | /* NGX_ERROR resets any pending data */ 496 | return (rc == NGX_OK) ? NGX_ERROR : rc; 497 | } 498 | 499 | return NGX_OK; 500 | } 501 | 502 | /* Send the data to the next header and body filter */ 503 | static ngx_int_t ngx_http_jpeg_filter_send(ngx_http_request_t *r, ngx_uint_t image) { 504 | ngx_buf_t *b; 505 | ngx_chain_t out; 506 | ngx_int_t rc; 507 | ngx_http_jpeg_filter_ctx_t *ctx; 508 | 509 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_filter_send"); 510 | 511 | ctx = ngx_http_get_module_ctx(r, ngx_http_jpeg_filter_module); 512 | 513 | /* Allocate memory for a buffer */ 514 | b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); 515 | if(b == NULL) { 516 | return NGX_ERROR; 517 | } 518 | 519 | if(image == NGX_HTTP_JPEG_FILTER_MODIFIED) { 520 | b->pos = ctx->out_image; 521 | b->last = ctx->out_last; 522 | } 523 | else { 524 | b->pos = ctx->in_image; 525 | b->last = ctx->in_last; 526 | } 527 | 528 | b->memory = 1; 529 | b->last_buf = 1; 530 | 531 | out.buf = b; 532 | out.next = NULL; 533 | 534 | /* Set the content type. However, this should be already the case, but better be sure */ 535 | r->headers_out.content_type.len = sizeof("image/jpeg") - 1; 536 | r->headers_out.content_type.data = (u_char *) "image/jpeg"; 537 | 538 | /* The content length must be adjusted */ 539 | r->headers_out.content_length_n = b->last - b->pos; 540 | 541 | /* No clue what is happening here. Copied from image filter module */ 542 | if(r->headers_out.content_length) { 543 | r->headers_out.content_length->hash = 0; 544 | } 545 | 546 | r->headers_out.content_length = NULL; 547 | 548 | /* 549 | * Now that we are done and we know the final size of the modified body 550 | * we can proceed to the next header filter. 551 | */ 552 | rc = ngx_http_next_header_filter(r); 553 | 554 | /* Bail out if something went wrong */ 555 | if(rc == NGX_ERROR || rc > NGX_OK || r->header_only) { 556 | return NGX_ERROR; 557 | } 558 | 559 | /* Push the modified body to the next body filter */ 560 | return ngx_http_next_body_filter(r, &out); 561 | } 562 | 563 | /* Test the incoming data if we can and should handle it */ 564 | static ngx_uint_t ngx_http_jpeg_filter_test(ngx_http_request_t *r, ngx_chain_t *in) { 565 | u_char *p; 566 | 567 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_filter_test"); 568 | 569 | /* 570 | * Checking if we have enough data available such that we can 571 | * decide if we can and should handle it. 572 | */ 573 | p = in->buf->pos; 574 | 575 | if(in->buf->last - p < 16) { 576 | return NGX_HTTP_IMAGE_NONE; 577 | } 578 | 579 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: \"%02xd%02xd\"", p[0], p[1]); 580 | 581 | /* Check for JPEG signature */ 582 | if(p[0] == 0xff && p[1] == 0xd8) { /* JPEG */ 583 | return NGX_HTTP_IMAGE_JPEG; 584 | } 585 | 586 | return NGX_HTTP_IMAGE_NONE; 587 | } 588 | 589 | /* Read several buffer chains and store the data in a buffer */ 590 | static ngx_int_t ngx_http_jpeg_filter_read(ngx_http_request_t *r, ngx_chain_t *in) { 591 | u_char *p; 592 | size_t size, rest; 593 | ngx_buf_t *b; 594 | ngx_chain_t *cl; 595 | ngx_http_jpeg_filter_ctx_t *ctx; 596 | 597 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_filter_read"); 598 | 599 | /* Get our context for this request */ 600 | ctx = ngx_http_get_module_ctx(r, ngx_http_jpeg_filter_module); 601 | 602 | /* If we didn't allocate yet memory for the image, we do it now */ 603 | if(ctx->in_image == NULL) { 604 | /* We found out the size of the buffer in the header filter */ 605 | ctx->in_image = ngx_palloc(r->pool, ctx->length); 606 | if (ctx->in_image == NULL) { 607 | return NGX_ERROR; 608 | } 609 | 610 | ctx->in_last = ctx->in_image; 611 | } 612 | 613 | p = ctx->in_last; 614 | 615 | /* Copying the data from the buffer chain into out buffer */ 616 | for(cl = in; cl; cl = cl->next) { 617 | b = cl->buf; 618 | size = b->last - b->pos; 619 | 620 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter buf: %uz", size); 621 | 622 | rest = ctx->in_image + ctx->length - p; 623 | 624 | if(size > rest) { 625 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "jpeg_filter: too big response"); 626 | return NGX_ERROR; 627 | } 628 | 629 | p = ngx_cpymem(p, b->pos, size); 630 | b->pos += size; 631 | 632 | if (b->last_buf) { 633 | ctx->in_last = p; 634 | 635 | /* If this was the last buffer chain, we're done */ 636 | return NGX_OK; 637 | } 638 | } 639 | 640 | ctx->in_last = p; 641 | 642 | /* This should be probably done, but no clue what it actually means */ 643 | r->connection->buffered |= NGX_HTTP_IMAGE_BUFFERED; 644 | 645 | /* This wasn't the las buffer chain. Tell the caller that we're expecting more */ 646 | return NGX_AGAIN; 647 | } 648 | 649 | /* Process the image */ 650 | static ngx_int_t ngx_http_jpeg_filter_process(ngx_http_request_t *r) { 651 | ngx_http_jpeg_filter_ctx_t *ctx; 652 | ngx_http_jpeg_filter_conf_t *conf; 653 | ngx_pool_cleanup_t *cln; 654 | 655 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: ngx_http_jpeg_filter_process"); 656 | 657 | /* No clue that this actually does. Copied from image filter module */ 658 | r->connection->buffered &= ~NGX_HTTP_IMAGE_BUFFERED; 659 | 660 | /* Get our context for this request */ 661 | ctx = ngx_http_get_module_ctx(r, ngx_http_jpeg_filter_module); 662 | if(ctx->in_image == NULL) { 663 | /* No data available. Bail out */ 664 | return NGX_ERROR; 665 | } 666 | 667 | /* Get out module configuration so we know what we actually have to do */ 668 | conf = ngx_http_get_module_loc_conf(r, ngx_http_jpeg_filter_module); 669 | 670 | /* Read the image */ 671 | mj_jpeg_t m; 672 | mj_init_jpeg(&m); 673 | 674 | if(mj_read_jpeg_from_memory(&m, ctx->in_image, ctx->length, conf->max_pixel) != MJ_OK) { 675 | mj_free_jpeg(&m); 676 | return NGX_ERROR; 677 | } 678 | 679 | ngx_http_jpeg_filter_element_t *felts = conf->filter_elements->elts; 680 | ngx_uint_t i; 681 | ngx_int_t n, align = 0, offset_x = 0, offset_y = 0; 682 | ngx_str_t val1, val2; 683 | mj_dropon_t d; 684 | 685 | /* Go through the processing chain */ 686 | for(i = 0; i < conf->filter_elements->nelts; i++) { 687 | switch(felts[i].type) { 688 | case NGX_HTTP_JPEG_FILTER_TYPE_EFFECT1: 689 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 690 | 691 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying effect '%s'", val1.data); 692 | 693 | if(ngx_strcmp(val1.data, "grayscale") == 0) { 694 | mj_effect_grayscale(&m); 695 | } 696 | else if(ngx_strcmp(val1.data, "pixelate") == 0) { 697 | mj_effect_pixelate(&m); 698 | } 699 | else { 700 | ngx_log_error(NGX_LOG_NOTICE, r->connection->log, 0, "jpeg_filter: invalid effect \"%s\"", val1.data); 701 | } 702 | 703 | break; 704 | case NGX_HTTP_JPEG_FILTER_TYPE_EFFECT2: 705 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 706 | 707 | n = ngx_http_jpeg_filter_get_int_value(r, &felts[i].cv2, 0); 708 | if(n < 0) { 709 | n = 0; 710 | } 711 | 712 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying effect '%s(%d)'", val1.data, n); 713 | 714 | if(ngx_strcmp(val1.data, "brighten") == 0) { 715 | mj_effect_luminance(&m, n); 716 | } 717 | else if(ngx_strcmp(val1.data, "darken") == 0) { 718 | mj_effect_luminance(&m, -n); 719 | } 720 | else if(ngx_strcmp(val1.data, "tintblue") == 0) { 721 | mj_effect_tint(&m, n, 0); 722 | } 723 | else if(ngx_strcmp(val1.data, "tintyellow") == 0) { 724 | mj_effect_tint(&m, -n, 0); 725 | } 726 | else if(ngx_strcmp(val1.data, "tintred") == 0) { 727 | mj_effect_tint(&m, 0, n); 728 | } 729 | else if(ngx_strcmp(val1.data, "tintgreen") == 0) { 730 | mj_effect_tint(&m, 0, -n); 731 | } 732 | else { 733 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: invalid effect \"%s\"", val1.data); 734 | } 735 | 736 | break; 737 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_ALIGN: 738 | align = 0; 739 | 740 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 741 | 742 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying dropon align '%s'", val1.data); 743 | 744 | if(ngx_strcmp(val1.data, "top") == 0) { 745 | align |= MJ_ALIGN_TOP; 746 | } 747 | else if(ngx_strcmp(val1.data, "bottom") == 0) { 748 | align |= MJ_ALIGN_BOTTOM; 749 | } 750 | else if(ngx_strcmp(val1.data, "center") == 0) { 751 | align |= MJ_ALIGN_CENTER; 752 | } 753 | else { 754 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: invalid alignment \"%s\"", val1.data); 755 | } 756 | 757 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv2, &val1); 758 | 759 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying dropon align '%s'", val1.data); 760 | 761 | if(ngx_strcmp(val1.data, "left") == 0) { 762 | align |= MJ_ALIGN_LEFT; 763 | } 764 | else if(ngx_strcmp(val1.data, "right") == 0) { 765 | align |= MJ_ALIGN_RIGHT; 766 | } 767 | else if(ngx_strcmp(val1.data, "center") == 0) { 768 | align |= MJ_ALIGN_CENTER; 769 | } 770 | else { 771 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: invalid alignment \"%s\"", val1.data); 772 | } 773 | 774 | break; 775 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_OFFSET: 776 | offset_y = ngx_http_jpeg_filter_get_int_value(r, &felts[i].cv1, offset_y); 777 | offset_x = ngx_http_jpeg_filter_get_int_value(r, &felts[i].cv2, offset_x); 778 | 779 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying dropon offset (%dpx,%dpx)", offset_y, offset_x); 780 | 781 | break; 782 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON: 783 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying preloaded dropon"); 784 | mj_compose(&m, felts[i].dropon, align, offset_x, offset_y); 785 | 786 | break; 787 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE1: 788 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE2: 789 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying dynamic dropon"); 790 | 791 | mj_init_dropon(&d); 792 | 793 | if(felts[i].type == NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE1) { 794 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 795 | 796 | if(mj_read_dropon_from_file(&d, (char *)val1.data, NULL, MJ_BLEND_FULL) != MJ_OK) { 797 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: dropon could not load the file \"%s\"", val1.data); 798 | } 799 | } 800 | else { 801 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 802 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv2, &val2); 803 | 804 | if(mj_read_dropon_from_file(&d, (char *)val1.data, (char *)val2.data, MJ_BLEND_FULL) != MJ_OK) { 805 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: dropon could not load the file \"%s\" or \"%s\"", val1.data, val2.data); 806 | } 807 | } 808 | 809 | mj_compose(&m, &d, align, offset_x, offset_y); 810 | 811 | mj_free_dropon(&d); 812 | 813 | break; 814 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY1: 815 | case NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY2: 816 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: applying dynamic dropon"); 817 | 818 | mj_init_dropon(&d); 819 | 820 | if(felts[i].type == NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY1) { 821 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 822 | 823 | if(mj_read_dropon_from_memory(&d, val1.data, val1.len, NULL, 0, MJ_BLEND_FULL) != MJ_OK) { 824 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: dropon could not load the bitstream"); 825 | } 826 | } 827 | else { 828 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv1, &val1); 829 | ngx_http_jpeg_filter_get_string_value(r, &felts[i].cv2, &val2); 830 | 831 | if(mj_read_dropon_from_memory(&d, val1.data, val1.len, val2.data, val2.len, MJ_BLEND_FULL) != MJ_OK) { 832 | ngx_log_error(NGX_LOG_WARN, r->connection->log, 0, "jpeg_filter: dropon could not load the bitstream"); 833 | } 834 | } 835 | 836 | mj_compose(&m, &d, align, offset_x, offset_y); 837 | 838 | mj_free_dropon(&d); 839 | 840 | break; 841 | default: 842 | break; 843 | } 844 | } 845 | 846 | /* Apply the options */ 847 | int options = 0; 848 | 849 | if(conf->optimize) { 850 | options |= MJ_OPTION_OPTIMIZE; 851 | } 852 | 853 | if(conf->progressive) { 854 | options |= MJ_OPTION_PROGRESSIVE; 855 | } 856 | 857 | if(conf->arithmetric) { 858 | options |= MJ_OPTION_ARITHMETRIC; 859 | } 860 | 861 | ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "jpeg_filter: JPEG output options %d", options); 862 | 863 | /* Write the modified image to a new buffer */ 864 | 865 | size_t len; 866 | 867 | if(mj_write_jpeg_to_memory(&m, &ctx->out_image, &len, options) != 0) { 868 | mj_free_jpeg(&m); 869 | return NGX_ERROR; 870 | } 871 | 872 | ctx->out_last = ctx->out_image + len; 873 | 874 | /* Destroy the modified image */ 875 | mj_free_jpeg(&m); 876 | 877 | /* 878 | * Add a cleanup routine for the allocated buffer that holds 879 | * the modified image. We can only destroy it safely after it has been send. 880 | */ 881 | cln = ngx_pool_cleanup_add(r->pool, 0); 882 | if(cln == NULL) { 883 | return NGX_ERROR; 884 | } 885 | 886 | cln->handler = ngx_http_jpeg_filter_cleanup; 887 | cln->data = ctx; 888 | 889 | return NGX_OK; 890 | } 891 | 892 | /* A function similar to ngx_atoi that can handle negative numbers */ 893 | static ngx_int_t ngx_atois(u_char *line, size_t n) { 894 | ngx_int_t value, sign, cutoff, cutlim; 895 | 896 | if(n == 0) { 897 | return NGX_ERROR; 898 | } 899 | 900 | cutoff = NGX_MAX_INT_T_VALUE / 10; 901 | cutlim = NGX_MAX_INT_T_VALUE % 10; 902 | 903 | sign = 1; 904 | if(*line == '-') { 905 | sign = -1; 906 | line++; 907 | n--; 908 | } 909 | 910 | for(value = 0; n--; line++) { 911 | if(*line < '0' || *line > '9') { 912 | return NGX_ERROR; 913 | } 914 | 915 | if(value >= cutoff && (value > cutoff || *line - '0' > cutlim)) { 916 | return NGX_ERROR; 917 | } 918 | 919 | value = value * 10 + (*line - '0'); 920 | } 921 | 922 | return (sign * value); 923 | } 924 | 925 | /* Interpret a complex value as an int */ 926 | static ngx_int_t ngx_http_jpeg_filter_get_int_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_int_t defval) { 927 | ngx_str_t val; 928 | ngx_int_t n; 929 | 930 | if(cv->lengths == NULL) { 931 | n = ngx_atois(cv->value.data, cv->value.len); 932 | } 933 | else { 934 | if(ngx_http_complex_value(r, cv, &val) != NGX_OK) { 935 | return defval; 936 | } 937 | 938 | /* Subtract 1 from the length because we compiled the complex value with 'zero=1' */ 939 | n = ngx_atois(val.data, val.len - 1); 940 | } 941 | 942 | return n; 943 | } 944 | 945 | /* Get the complex value as a string */ 946 | static ngx_int_t ngx_http_jpeg_filter_get_string_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_str_t *val) { 947 | return ngx_http_complex_value(r, cv, val); 948 | } 949 | 950 | /* Cleanup after the request finished */ 951 | static void ngx_http_jpeg_filter_cleanup(void *data) { 952 | ngx_http_jpeg_filter_ctx_t *ctx = data; 953 | 954 | if(ctx->out_image != NULL) { 955 | free(ctx->out_image); 956 | } 957 | 958 | return; 959 | } 960 | 961 | /* Process the "jpeg_filter_effect" configuration directives */ 962 | static char *ngx_conf_jpeg_filter_effect(ngx_conf_t *cf, ngx_command_t *cmd, void *c) { 963 | ngx_http_jpeg_filter_conf_t *conf = c; 964 | 965 | ngx_str_t *value; 966 | ngx_http_compile_complex_value_t ccv; 967 | ngx_http_jpeg_filter_element_t *fe; 968 | 969 | ngx_log_debug0(NGX_LOG_DEBUG_CORE, cf->log, 0, "jpeg_filter: ngx_conf_jpeg_filter_effect"); 970 | 971 | value = cf->args->elts; 972 | 973 | /* Initialize the processing chain */ 974 | if(conf->filter_elements == NULL) { 975 | conf->filter_elements = ngx_array_create(cf->pool, 10, sizeof(ngx_http_jpeg_filter_element_t)); 976 | if(conf->filter_elements == NULL) { 977 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to create filter chain"); 978 | return NGX_CONF_ERROR; 979 | } 980 | } 981 | 982 | /* Add a new element to the processing chain */ 983 | fe = (ngx_http_jpeg_filter_element_t *)ngx_array_push(conf->filter_elements); 984 | if(fe == NULL) { 985 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to add new filter to filter chain for \"%s\"", value[0].data); 986 | return NGX_CONF_ERROR; 987 | } 988 | 989 | ngx_memzero(fe, sizeof(ngx_http_jpeg_filter_element_t)); 990 | 991 | if(cf->args->nelts == 2) { 992 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_EFFECT1; 993 | 994 | /* Get the effect name as complex value */ 995 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 996 | 997 | ccv.cf = cf; 998 | ccv.value = &value[1]; 999 | ccv.complex_value = &fe->cv1; 1000 | ccv.zero = 1; 1001 | 1002 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1003 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value \"%s %s\"", value[0].data, value[1].data); 1004 | return NGX_CONF_ERROR; 1005 | } 1006 | } 1007 | else if(cf->args->nelts == 3) { 1008 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_EFFECT2; 1009 | 1010 | /* Get the effect name as complex value */ 1011 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1012 | 1013 | ccv.cf = cf; 1014 | ccv.value = &value[1]; 1015 | ccv.complex_value = &fe->cv1; 1016 | ccv.zero = 1; 1017 | 1018 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1019 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1020 | return NGX_CONF_ERROR; 1021 | } 1022 | 1023 | /* Get the effect value as complex value */ 1024 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1025 | 1026 | ccv.cf = cf; 1027 | ccv.value = &value[2]; 1028 | ccv.complex_value = &fe->cv2; 1029 | ccv.zero = 1; 1030 | 1031 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1032 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1033 | return NGX_CONF_ERROR; 1034 | } 1035 | } 1036 | 1037 | return NGX_CONF_OK; 1038 | } 1039 | 1040 | /* Process the "jpeg_filter_dropon*" configuration directives */ 1041 | static char *ngx_conf_jpeg_filter_dropon(ngx_conf_t *cf, ngx_command_t *cmd, void *c) { 1042 | ngx_http_jpeg_filter_conf_t *conf = c; 1043 | 1044 | ngx_str_t *value; 1045 | ngx_http_compile_complex_value_t ccv; 1046 | ngx_http_jpeg_filter_element_t *fe; 1047 | 1048 | ngx_log_debug0(NGX_LOG_DEBUG_CORE, cf->log, 0, "jpeg_filter: ngx_conf_jpeg_filter_dropon"); 1049 | 1050 | value = cf->args->elts; 1051 | 1052 | /* Initialize the processing chain */ 1053 | if(conf->filter_elements == NULL) { 1054 | conf->filter_elements = ngx_array_create(cf->pool, 10, sizeof(ngx_http_jpeg_filter_element_t)); 1055 | if(conf->filter_elements == NULL) { 1056 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to create filter chain"); 1057 | return NGX_CONF_ERROR; 1058 | } 1059 | } 1060 | 1061 | /* Add a new element to the processing chain */ 1062 | fe = (ngx_http_jpeg_filter_element_t *)ngx_array_push(conf->filter_elements); 1063 | if(fe == NULL) { 1064 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to add new filter to filter chain (%s)", value[0].data); 1065 | return NGX_CONF_ERROR; 1066 | } 1067 | 1068 | ngx_memzero(fe, sizeof(ngx_http_jpeg_filter_element_t)); 1069 | 1070 | if(ngx_strcmp(value[0].data, "jpeg_filter_dropon_align") == 0) { 1071 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_ALIGN; 1072 | 1073 | /* Vertical alignment (top, bottom, center) */ 1074 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1075 | 1076 | ccv.cf = cf; 1077 | ccv.value = &value[1]; 1078 | ccv.complex_value = &fe->cv1; 1079 | ccv.zero = 1; 1080 | 1081 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1082 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1083 | return NGX_CONF_ERROR; 1084 | } 1085 | 1086 | /* Horizontal alignment (left, right, center) */ 1087 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1088 | 1089 | ccv.cf = cf; 1090 | ccv.value = &value[2]; 1091 | ccv.complex_value = &fe->cv2; 1092 | ccv.zero = 1; 1093 | 1094 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1095 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1096 | return NGX_CONF_ERROR; 1097 | } 1098 | } 1099 | else if(ngx_strcmp(value[0].data, "jpeg_filter_dropon_offset") == 0) { 1100 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_OFFSET; 1101 | 1102 | /* Vertical offset */ 1103 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1104 | 1105 | ccv.cf = cf; 1106 | ccv.value = &value[1]; 1107 | ccv.complex_value = &fe->cv1; 1108 | ccv.zero = 1; 1109 | 1110 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1111 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1112 | return NGX_CONF_ERROR; 1113 | } 1114 | 1115 | /* Horizontal offset */ 1116 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1117 | 1118 | ccv.cf = cf; 1119 | ccv.value = &value[2]; 1120 | ccv.complex_value = &fe->cv2; 1121 | ccv.zero = 1; 1122 | 1123 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1124 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1125 | return NGX_CONF_ERROR; 1126 | } 1127 | } 1128 | else if(ngx_strcmp(value[0].data, "jpeg_filter_dropon_file") == 0) { 1129 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON; 1130 | 1131 | ngx_int_t has_variables = 0; 1132 | 1133 | /* Dropon */ 1134 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1135 | 1136 | ccv.cf = cf; 1137 | ccv.value = &value[1]; 1138 | ccv.complex_value = &fe->cv1; 1139 | ccv.zero = 1; 1140 | 1141 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1142 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1143 | return NGX_CONF_ERROR; 1144 | } 1145 | 1146 | if(fe->cv1.lengths != NULL) { 1147 | has_variables = 1; 1148 | } 1149 | 1150 | if(cf->args->nelts == 3) { 1151 | /* Mask */ 1152 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1153 | 1154 | ccv.cf = cf; 1155 | ccv.value = &value[2]; 1156 | ccv.complex_value = &fe->cv2; 1157 | ccv.zero = 1; 1158 | 1159 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1160 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1161 | return NGX_CONF_ERROR; 1162 | } 1163 | 1164 | if(fe->cv2.lengths != NULL) { 1165 | has_variables = 1; 1166 | } 1167 | } 1168 | 1169 | /* Check if there are any variables in the values */ 1170 | if(has_variables == 0) { 1171 | fe->dropon = (mj_dropon_t *)ngx_palloc(cf->pool, sizeof(mj_dropon_t)); 1172 | if(fe->dropon == NULL) { 1173 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: could not allocate memory for dropon"); 1174 | return NGX_CONF_ERROR; 1175 | } 1176 | 1177 | mj_init_dropon(fe->dropon); 1178 | 1179 | if(cf->args->nelts == 2) { 1180 | /* Dropon without a mask */ 1181 | if(mj_read_dropon_from_file(fe->dropon, (char *)value[1].data, NULL, MJ_BLEND_FULL) != MJ_OK) { 1182 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: dropon could not load the file \"%s\"", value[1].data); 1183 | return NGX_CONF_ERROR; 1184 | } 1185 | } 1186 | else { 1187 | /* Dropon with a mask */ 1188 | if(mj_read_dropon_from_file(fe->dropon, (char *)value[1].data, (char *)value[2].data, MJ_BLEND_FULL) != MJ_OK) { 1189 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: dropon could not load the file \"%s\" or \"%s\"", value[1].data, value[2].data); 1190 | return NGX_CONF_ERROR; 1191 | } 1192 | } 1193 | 1194 | /* Add a cleanup routine for the allocated dropon */ 1195 | ngx_pool_cleanup_t *cln; 1196 | 1197 | cln = ngx_pool_cleanup_add(cf->pool, 0); 1198 | if(cln == NULL) { 1199 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to add cleanup routine for dropon"); 1200 | return NGX_CONF_ERROR; 1201 | } 1202 | 1203 | cln->handler = ngx_http_jpeg_filter_conf_cleanup; 1204 | cln->data = fe->dropon; 1205 | } 1206 | else { 1207 | if(cf->args->nelts == 2) { 1208 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE1; 1209 | } 1210 | else { 1211 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_FILE2; 1212 | } 1213 | 1214 | fe->dropon = NULL; 1215 | } 1216 | } 1217 | else if(ngx_strcmp(value[0].data, "jpeg_filter_dropon_memory") == 0) { 1218 | /* Dropon */ 1219 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1220 | 1221 | ccv.cf = cf; 1222 | ccv.value = &value[1]; 1223 | ccv.complex_value = &fe->cv1; 1224 | ccv.zero = 1; 1225 | 1226 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1227 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1228 | return NGX_CONF_ERROR; 1229 | } 1230 | 1231 | if(cf->args->nelts == 3) { 1232 | /* Mask */ 1233 | ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); 1234 | 1235 | ccv.cf = cf; 1236 | ccv.value = &value[2]; 1237 | ccv.complex_value = &fe->cv2; 1238 | ccv.zero = 1; 1239 | 1240 | if(ngx_http_compile_complex_value(&ccv) != NGX_OK) { 1241 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to compile complex value for \"%s %s %s\"", value[0].data, value[1].data, value[2].data); 1242 | return NGX_CONF_ERROR; 1243 | } 1244 | } 1245 | 1246 | if(cf->args->nelts == 2) { 1247 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY1; 1248 | } 1249 | else { 1250 | fe->type = NGX_HTTP_JPEG_FILTER_TYPE_DROPON_MEMORY2; 1251 | } 1252 | 1253 | fe->dropon = NULL; 1254 | } 1255 | 1256 | return NGX_CONF_OK; 1257 | } 1258 | 1259 | /* Cleanup stuff was allocated without a pool during configuration */ 1260 | static void ngx_http_jpeg_filter_conf_cleanup(void *data) { 1261 | mj_dropon_t *d = (mj_dropon_t *)data; 1262 | 1263 | mj_free_dropon(d); 1264 | 1265 | return; 1266 | } 1267 | 1268 | static void *ngx_http_jpeg_filter_create_conf(ngx_conf_t *cf) { 1269 | ngx_http_jpeg_filter_conf_t *conf; 1270 | 1271 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_jpeg_filter_conf_t)); 1272 | if(conf == NULL) { 1273 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "jpeg_filter: failed to allocate memory for filter config"); 1274 | return NGX_CONF_ERROR; 1275 | } 1276 | 1277 | conf->max_pixel = NGX_CONF_UNSET_UINT; 1278 | 1279 | conf->enable = NGX_CONF_UNSET; 1280 | conf->optimize = NGX_CONF_UNSET; 1281 | conf->progressive = NGX_CONF_UNSET; 1282 | conf->graceful = NGX_CONF_UNSET; 1283 | 1284 | conf->buffer_size = NGX_CONF_UNSET_SIZE; 1285 | 1286 | return conf; 1287 | } 1288 | 1289 | static char *ngx_http_jpeg_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child) { 1290 | ngx_http_jpeg_filter_conf_t *prev = parent; 1291 | ngx_http_jpeg_filter_conf_t *conf = child; 1292 | 1293 | ngx_conf_merge_uint_value(conf->max_pixel, prev->max_pixel, 0); 1294 | 1295 | ngx_conf_merge_value(conf->enable, prev->enable, 0); 1296 | ngx_conf_merge_value(conf->optimize, prev->optimize, 0); 1297 | ngx_conf_merge_value(conf->progressive, prev->progressive, 0); 1298 | ngx_conf_merge_value(conf->graceful, prev->graceful, 0); 1299 | 1300 | ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, NGX_HTTP_JPEG_FILTER_BUFFER_SIZE); 1301 | 1302 | return NGX_CONF_OK; 1303 | } 1304 | 1305 | static ngx_int_t ngx_http_jpeg_filter_init(ngx_conf_t *cf) { 1306 | ngx_http_next_header_filter = ngx_http_top_header_filter; 1307 | ngx_http_top_header_filter = ngx_http_jpeg_header_filter; 1308 | 1309 | ngx_http_next_body_filter = ngx_http_top_body_filter; 1310 | ngx_http_top_body_filter = ngx_http_jpeg_body_filter; 1311 | 1312 | return NGX_OK; 1313 | } 1314 | --------------------------------------------------------------------------------