/* * Copyright (C) Igor Sysoev * Copyright (C) Nginx, Inc. */ #include #include #include #include #define NGX_HTTP_IMAGE_OFF 0 #define NGX_HTTP_IMAGE_TEST 1 #define NGX_HTTP_IMAGE_SIZE 2 #define NGX_HTTP_IMAGE_RESIZE 3 #define NGX_HTTP_IMAGE_CROP 4 #define NGX_HTTP_IMAGE_ROTATE 5 #define NGX_HTTP_IMAGE_START 0 #define NGX_HTTP_IMAGE_READ 1 #define NGX_HTTP_IMAGE_PROCESS 2 #define NGX_HTTP_IMAGE_PASS 3 #define NGX_HTTP_IMAGE_DONE 4 #define NGX_HTTP_IMAGE_NONE 0 #define NGX_HTTP_IMAGE_JPEG 1 #define NGX_HTTP_IMAGE_GIF 2 #define NGX_HTTP_IMAGE_PNG 3 #define NGX_HTTP_IMAGE_WEBP 4 #define NGX_HTTP_IMAGE_BUFFERED 0x08 typedef struct { ngx_uint_t filter; ngx_uint_t width; ngx_uint_t height; ngx_uint_t angle; ngx_uint_t jpeg_quality; ngx_uint_t webp_quality; ngx_uint_t sharpen; ngx_flag_t transparency; ngx_flag_t interlace; ngx_http_complex_value_t *wcv; ngx_http_complex_value_t *hcv; ngx_http_complex_value_t *acv; ngx_http_complex_value_t *jqcv; ngx_http_complex_value_t *wqcv; ngx_http_complex_value_t *shcv; size_t buffer_size; } ngx_http_image_filter_conf_t; typedef struct { u_char *image; u_char *last; size_t length; ngx_uint_t width; ngx_uint_t height; ngx_uint_t max_width; ngx_uint_t max_height; ngx_uint_t angle; ngx_uint_t phase; ngx_uint_t type; ngx_uint_t force; } ngx_http_image_filter_ctx_t; static ngx_int_t ngx_http_image_send(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx, ngx_chain_t *in); static ngx_uint_t ngx_http_image_test(ngx_http_request_t *r, ngx_chain_t *in); static ngx_int_t ngx_http_image_read(ngx_http_request_t *r, ngx_chain_t *in); static ngx_buf_t *ngx_http_image_process(ngx_http_request_t *r); static ngx_buf_t *ngx_http_image_json(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx); static ngx_buf_t *ngx_http_image_asis(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx); static void ngx_http_image_length(ngx_http_request_t *r, ngx_buf_t *b); static ngx_int_t ngx_http_image_size(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx); static ngx_buf_t *ngx_http_image_resize(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx); static gdImagePtr ngx_http_image_source(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx); static gdImagePtr ngx_http_image_new(ngx_http_request_t *r, int w, int h, int colors); static u_char *ngx_http_image_out(ngx_http_request_t *r, ngx_uint_t type, gdImagePtr img, int *size); static void ngx_http_image_cleanup(void *data); static ngx_uint_t ngx_http_image_filter_get_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_uint_t v); static ngx_uint_t ngx_http_image_filter_value(ngx_str_t *value); static void *ngx_http_image_filter_create_conf(ngx_conf_t *cf); static char *ngx_http_image_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child); static char *ngx_http_image_filter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_image_filter_jpeg_quality(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_image_filter_webp_quality(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static char *ngx_http_image_filter_sharpen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static ngx_int_t ngx_http_image_filter_init(ngx_conf_t *cf); static ngx_command_t ngx_http_image_filter_commands[] = { { ngx_string("image_filter"), NGX_HTTP_LOC_CONF|NGX_CONF_TAKE123, ngx_http_image_filter, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, { ngx_string("image_filter_jpeg_quality"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_image_filter_jpeg_quality, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, { ngx_string("image_filter_webp_quality"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_image_filter_webp_quality, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, { ngx_string("image_filter_sharpen"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_http_image_filter_sharpen, NGX_HTTP_LOC_CONF_OFFSET, 0, NULL }, { ngx_string("image_filter_transparency"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_image_filter_conf_t, transparency), NULL }, { ngx_string("image_filter_interlace"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_image_filter_conf_t, interlace), NULL }, { ngx_string("image_filter_buffer"), NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1, ngx_conf_set_size_slot, NGX_HTTP_LOC_CONF_OFFSET, offsetof(ngx_http_image_filter_conf_t, buffer_size), NULL }, ngx_null_command }; static ngx_http_module_t ngx_http_image_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_image_filter_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_image_filter_create_conf, /* create location configuration */ ngx_http_image_filter_merge_conf /* merge location configuration */ }; ngx_module_t ngx_http_image_filter_module = { NGX_MODULE_V1, &ngx_http_image_filter_module_ctx, /* module context */ ngx_http_image_filter_commands, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static ngx_http_output_header_filter_pt ngx_http_next_header_filter; static ngx_http_output_body_filter_pt ngx_http_next_body_filter; static ngx_str_t ngx_http_image_types[] = { ngx_string("image/jpeg"), ngx_string("image/gif"), ngx_string("image/png"), ngx_string("image/webp") }; static ngx_int_t ngx_http_image_header_filter(ngx_http_request_t *r) { off_t len; ngx_http_image_filter_ctx_t *ctx; ngx_http_image_filter_conf_t *conf; if (r->headers_out.status == NGX_HTTP_NOT_MODIFIED) { return ngx_http_next_header_filter(r); } ctx = ngx_http_get_module_ctx(r, ngx_http_image_filter_module); if (ctx) { ngx_http_set_ctx(r, NULL, ngx_http_image_filter_module); return ngx_http_next_header_filter(r); } conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); if (conf->filter == NGX_HTTP_IMAGE_OFF) { return ngx_http_next_header_filter(r); } if (r->headers_out.content_type.len >= sizeof("multipart/x-mixed-replace") - 1 && ngx_strncasecmp(r->headers_out.content_type.data, (u_char *) "multipart/x-mixed-replace", sizeof("multipart/x-mixed-replace") - 1) == 0) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "image filter: multipart/x-mixed-replace response"); return NGX_ERROR; } ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_image_filter_ctx_t)); if (ctx == NULL) { return NGX_ERROR; } ngx_http_set_ctx(r, ctx, ngx_http_image_filter_module); len = r->headers_out.content_length_n; if (len != -1 && len > (off_t) conf->buffer_size) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "image filter: too big response: %O", len); return NGX_HTTP_UNSUPPORTED_MEDIA_TYPE; } if (len == -1) { ctx->length = conf->buffer_size; } else { ctx->length = (size_t) len; } if (r->headers_out.refresh) { r->headers_out.refresh->hash = 0; } r->main_filter_need_in_memory = 1; r->allow_ranges = 0; return NGX_OK; } static ngx_int_t ngx_http_image_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { ngx_int_t rc; ngx_str_t *ct; ngx_chain_t out; ngx_http_image_filter_ctx_t *ctx; ngx_http_image_filter_conf_t *conf; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image filter"); if (in == NULL) { return ngx_http_next_body_filter(r, in); } ctx = ngx_http_get_module_ctx(r, ngx_http_image_filter_module); if (ctx == NULL) { return ngx_http_next_body_filter(r, in); } switch (ctx->phase) { case NGX_HTTP_IMAGE_START: ctx->type = ngx_http_image_test(r, in); conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); if (ctx->type == NGX_HTTP_IMAGE_NONE) { if (conf->filter == NGX_HTTP_IMAGE_SIZE) { out.buf = ngx_http_image_json(r, NULL); if (out.buf) { out.next = NULL; ctx->phase = NGX_HTTP_IMAGE_DONE; return ngx_http_image_send(r, ctx, &out); } } return ngx_http_filter_finalize_request(r, &ngx_http_image_filter_module, NGX_HTTP_UNSUPPORTED_MEDIA_TYPE); } /* override content type */ ct = &ngx_http_image_types[ctx->type - 1]; r->headers_out.content_type_len = ct->len; r->headers_out.content_type = *ct; r->headers_out.content_type_lowcase = NULL; if (conf->filter == NGX_HTTP_IMAGE_TEST) { ctx->phase = NGX_HTTP_IMAGE_PASS; return ngx_http_image_send(r, ctx, in); } ctx->phase = NGX_HTTP_IMAGE_READ; /* fall through */ case NGX_HTTP_IMAGE_READ: rc = ngx_http_image_read(r, in); if (rc == NGX_AGAIN) { return NGX_OK; } if (rc == NGX_ERROR) { return ngx_http_filter_finalize_request(r, &ngx_http_image_filter_module, NGX_HTTP_UNSUPPORTED_MEDIA_TYPE); } /* fall through */ case NGX_HTTP_IMAGE_PROCESS: out.buf = ngx_http_image_process(r); if (out.buf == NULL) { return ngx_http_filter_finalize_request(r, &ngx_http_image_filter_module, NGX_HTTP_UNSUPPORTED_MEDIA_TYPE); } out.next = NULL; ctx->phase = NGX_HTTP_IMAGE_PASS; return ngx_http_image_send(r, ctx, &out); case NGX_HTTP_IMAGE_PASS: return ngx_http_next_body_filter(r, in); default: /* NGX_HTTP_IMAGE_DONE */ rc = ngx_http_next_body_filter(r, NULL); /* NGX_ERROR resets any pending data */ return (rc == NGX_OK) ? NGX_ERROR : rc; } } static ngx_int_t ngx_http_image_send(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx, ngx_chain_t *in) { ngx_int_t rc; rc = ngx_http_next_header_filter(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { return NGX_ERROR; } rc = ngx_http_next_body_filter(r, in); if (ctx->phase == NGX_HTTP_IMAGE_DONE) { /* NGX_ERROR resets any pending data */ return (rc == NGX_OK) ? NGX_ERROR : rc; } return rc; } static ngx_uint_t ngx_http_image_test(ngx_http_request_t *r, ngx_chain_t *in) { u_char *p; p = in->buf->pos; if (in->buf->last - p < 16) { return NGX_HTTP_IMAGE_NONE; } ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image filter: \"%c%c\"", p[0], p[1]); if (p[0] == 0xff && p[1] == 0xd8) { /* JPEG */ return NGX_HTTP_IMAGE_JPEG; } else if (p[0] == 'G' && p[1] == 'I' && p[2] == 'F' && p[3] == '8' && p[5] == 'a') { if (p[4] == '9' || p[4] == '7') { /* GIF */ return NGX_HTTP_IMAGE_GIF; } } else if (p[0] == 0x89 && p[1] == 'P' && p[2] == 'N' && p[3] == 'G' && p[4] == 0x0d && p[5] == 0x0a && p[6] == 0x1a && p[7] == 0x0a) { /* PNG */ return NGX_HTTP_IMAGE_PNG; } else if (p[0] == 'R' && p[1] == 'I' && p[2] == 'F' && p[3] == 'F' && p[8] == 'W' && p[9] == 'E' && p[10] == 'B' && p[11] == 'P') { /* WebP */ return NGX_HTTP_IMAGE_WEBP; } return NGX_HTTP_IMAGE_NONE; } static ngx_int_t ngx_http_image_read(ngx_http_request_t *r, ngx_chain_t *in) { u_char *p; size_t size, rest; ngx_buf_t *b; ngx_chain_t *cl; ngx_http_image_filter_ctx_t *ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_image_filter_module); if (ctx->image == NULL) { ctx->image = ngx_palloc(r->pool, ctx->length); if (ctx->image == NULL) { return NGX_ERROR; } ctx->last = ctx->image; } p = ctx->last; for (cl = in; cl; cl = cl->next) { b = cl->buf; size = b->last - b->pos; ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image buf: %uz", size); rest = ctx->image + ctx->length - p; if (size > rest) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "image filter: too big response"); return NGX_ERROR; } p = ngx_cpymem(p, b->pos, size); b->pos += size; if (b->last_buf) { ctx->last = p; return NGX_OK; } } ctx->last = p; r->connection->buffered |= NGX_HTTP_IMAGE_BUFFERED; return NGX_AGAIN; } static ngx_buf_t * ngx_http_image_process(ngx_http_request_t *r) { ngx_int_t rc; ngx_http_image_filter_ctx_t *ctx; ngx_http_image_filter_conf_t *conf; r->connection->buffered &= ~NGX_HTTP_IMAGE_BUFFERED; ctx = ngx_http_get_module_ctx(r, ngx_http_image_filter_module); rc = ngx_http_image_size(r, ctx); conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); if (conf->filter == NGX_HTTP_IMAGE_SIZE) { return ngx_http_image_json(r, rc == NGX_OK ? ctx : NULL); } ctx->angle = ngx_http_image_filter_get_value(r, conf->acv, conf->angle); if (conf->filter == NGX_HTTP_IMAGE_ROTATE) { if (ctx->angle != 90 && ctx->angle != 180 && ctx->angle != 270) { return NULL; } return ngx_http_image_resize(r, ctx); } ctx->max_width = ngx_http_image_filter_get_value(r, conf->wcv, conf->width); if (ctx->max_width == 0) { return NULL; } ctx->max_height = ngx_http_image_filter_get_value(r, conf->hcv, conf->height); if (ctx->max_height == 0) { return NULL; } if (rc == NGX_OK && ctx->width <= ctx->max_width && ctx->height <= ctx->max_height && ctx->angle == 0 && !ctx->force) { return ngx_http_image_asis(r, ctx); } return ngx_http_image_resize(r, ctx); } static ngx_buf_t * ngx_http_image_json(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx) { size_t len; ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NULL; } b->memory = 1; b->last_buf = 1; ngx_http_clean_header(r); r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_type_len = sizeof("application/json") - 1; ngx_str_set(&r->headers_out.content_type, "application/json"); r->headers_out.content_type_lowcase = NULL; if (ctx == NULL) { b->pos = (u_char *) "{}" CRLF; b->last = b->pos + sizeof("{}" CRLF) - 1; ngx_http_image_length(r, b); return b; } len = sizeof("{ \"img\" : " "{ \"width\": , \"height\": , \"type\": \"jpeg\" } }" CRLF) - 1 + 2 * NGX_SIZE_T_LEN; b->pos = ngx_pnalloc(r->pool, len); if (b->pos == NULL) { return NULL; } b->last = ngx_sprintf(b->pos, "{ \"img\" : " "{ \"width\": %uz," " \"height\": %uz," " \"type\": \"%s\" } }" CRLF, ctx->width, ctx->height, ngx_http_image_types[ctx->type - 1].data + 6); ngx_http_image_length(r, b); return b; } static ngx_buf_t * ngx_http_image_asis(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx) { ngx_buf_t *b; b = ngx_calloc_buf(r->pool); if (b == NULL) { return NULL; } b->pos = ctx->image; b->last = ctx->last; b->memory = 1; b->last_buf = 1; ngx_http_image_length(r, b); return b; } static void ngx_http_image_length(ngx_http_request_t *r, ngx_buf_t *b) { r->headers_out.content_length_n = b->last - b->pos; if (r->headers_out.content_length) { r->headers_out.content_length->hash = 0; } r->headers_out.content_length = NULL; } static ngx_int_t ngx_http_image_size(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx) { u_char *p, *last; size_t len, app; ngx_uint_t width, height; p = ctx->image; switch (ctx->type) { case NGX_HTTP_IMAGE_JPEG: p += 2; last = ctx->image + ctx->length - 10; width = 0; height = 0; app = 0; while (p < last) { if (p[0] == 0xff && p[1] != 0xff) { ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "JPEG: %02xd %02xd", p[0], p[1]); p++; if ((*p == 0xc0 || *p == 0xc1 || *p == 0xc2 || *p == 0xc3 || *p == 0xc9 || *p == 0xca || *p == 0xcb) && (width == 0 || height == 0)) { width = p[6] * 256 + p[7]; height = p[4] * 256 + p[5]; } ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "JPEG: %02xd %02xd", p[1], p[2]); len = p[1] * 256 + p[2]; if (*p >= 0xe1 && *p <= 0xef) { /* application data, e.g., EXIF, Adobe XMP, etc. */ app += len; } p += len; continue; } p++; } if (width == 0 || height == 0) { return NGX_DECLINED; } if (ctx->length / 20 < app) { /* force conversion if application data consume more than 5% */ ctx->force = 1; ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "app data size: %uz", app); } break; case NGX_HTTP_IMAGE_GIF: if (ctx->length < 10) { return NGX_DECLINED; } width = p[7] * 256 + p[6]; height = p[9] * 256 + p[8]; break; case NGX_HTTP_IMAGE_PNG: if (ctx->length < 24) { return NGX_DECLINED; } width = p[18] * 256 + p[19]; height = p[22] * 256 + p[23]; break; case NGX_HTTP_IMAGE_WEBP: if (ctx->length < 30) { return NGX_DECLINED; } if (p[12] != 'V' || p[13] != 'P' || p[14] != '8') { return NGX_DECLINED; } switch (p[15]) { case ' ': if (p[20] & 1) { /* not a key frame */ return NGX_DECLINED; } if (p[23] != 0x9d || p[24] != 0x01 || p[25] != 0x2a) { /* invalid start code */ return NGX_DECLINED; } width = (p[26] | p[27] << 8) & 0x3fff; height = (p[28] | p[29] << 8) & 0x3fff; break; case 'L': if (p[20] != 0x2f) { /* invalid signature */ return NGX_DECLINED; } width = ((p[21] | p[22] << 8) & 0x3fff) + 1; height = ((p[22] >> 6 | p[23] << 2 | p[24] << 10) & 0x3fff) + 1; break; case 'X': width = (p[24] | p[25] << 8 | p[26] << 16) + 1; height = (p[27] | p[28] << 8 | p[29] << 16) + 1; break; default: return NGX_DECLINED; } break; default: return NGX_DECLINED; } ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image size: %d x %d", (int) width, (int) height); ctx->width = width; ctx->height = height; return NGX_OK; } static ngx_buf_t * ngx_http_image_resize(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx) { int sx, sy, dx, dy, ox, oy, ax, ay, size, colors, palette, transparent, sharpen, red, green, blue, t; u_char *out; ngx_buf_t *b; ngx_uint_t resize; gdImagePtr src, dst; ngx_pool_cleanup_t *cln; ngx_http_image_filter_conf_t *conf; src = ngx_http_image_source(r, ctx); if (src == NULL) { return NULL; } sx = gdImageSX(src); sy = gdImageSY(src); conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); if (!ctx->force && ctx->angle == 0 && (ngx_uint_t) sx <= ctx->max_width && (ngx_uint_t) sy <= ctx->max_height) { gdImageDestroy(src); return ngx_http_image_asis(r, ctx); } colors = gdImageColorsTotal(src); if (colors && conf->transparency) { transparent = gdImageGetTransparent(src); if (transparent != -1) { palette = colors; red = gdImageRed(src, transparent); green = gdImageGreen(src, transparent); blue = gdImageBlue(src, transparent); goto transparent; } } palette = 0; transparent = -1; red = 0; green = 0; blue = 0; transparent: gdImageColorTransparent(src, -1); dx = sx; dy = sy; if (conf->filter == NGX_HTTP_IMAGE_RESIZE) { if ((ngx_uint_t) dx > ctx->max_width) { dy = dy * ctx->max_width / dx; dy = dy ? dy : 1; dx = ctx->max_width; } if ((ngx_uint_t) dy > ctx->max_height) { dx = dx * ctx->max_height / dy; dx = dx ? dx : 1; dy = ctx->max_height; } resize = 1; } else if (conf->filter == NGX_HTTP_IMAGE_ROTATE) { resize = 0; } else { /* NGX_HTTP_IMAGE_CROP */ resize = 0; if ((double) dx / dy < (double) ctx->max_width / ctx->max_height) { if ((ngx_uint_t) dx > ctx->max_width) { dy = dy * ctx->max_width / dx; dy = dy ? dy : 1; dx = ctx->max_width; resize = 1; } } else { if ((ngx_uint_t) dy > ctx->max_height) { dx = dx * ctx->max_height / dy; dx = dx ? dx : 1; dy = ctx->max_height; resize = 1; } } } if (resize) { dst = ngx_http_image_new(r, dx, dy, palette); if (dst == NULL) { gdImageDestroy(src); return NULL; } if (colors == 0) { gdImageSaveAlpha(dst, 1); gdImageAlphaBlending(dst, 0); } gdImageCopyResampled(dst, src, 0, 0, 0, 0, dx, dy, sx, sy); if (colors) { gdImageTrueColorToPalette(dst, 1, 256); } gdImageDestroy(src); } else { dst = src; } if (ctx->angle) { src = dst; ax = (dx % 2 == 0) ? 1 : 0; ay = (dy % 2 == 0) ? 1 : 0; switch (ctx->angle) { case 90: case 270: dst = ngx_http_image_new(r, dy, dx, palette); if (dst == NULL) { gdImageDestroy(src); return NULL; } if (ctx->angle == 90) { ox = dy / 2 + ay; oy = dx / 2 - ax; } else { ox = dy / 2 - ay; oy = dx / 2 + ax; } gdImageCopyRotated(dst, src, ox, oy, 0, 0, dx + ax, dy + ay, ctx->angle); gdImageDestroy(src); t = dx; dx = dy; dy = t; break; case 180: dst = ngx_http_image_new(r, dx, dy, palette); if (dst == NULL) { gdImageDestroy(src); return NULL; } gdImageCopyRotated(dst, src, dx / 2 - ax, dy / 2 - ay, 0, 0, dx + ax, dy + ay, ctx->angle); gdImageDestroy(src); break; } } if (conf->filter == NGX_HTTP_IMAGE_CROP) { src = dst; if ((ngx_uint_t) dx > ctx->max_width) { ox = dx - ctx->max_width; } else { ox = 0; } if ((ngx_uint_t) dy > ctx->max_height) { oy = dy - ctx->max_height; } else { oy = 0; } if (ox || oy) { dst = ngx_http_image_new(r, dx - ox, dy - oy, colors); if (dst == NULL) { gdImageDestroy(src); return NULL; } ox /= 2; oy /= 2; ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image crop: %d x %d @ %d x %d", dx, dy, ox, oy); if (colors == 0) { gdImageSaveAlpha(dst, 1); gdImageAlphaBlending(dst, 0); } gdImageCopy(dst, src, 0, 0, ox, oy, dx - ox, dy - oy); if (colors) { gdImageTrueColorToPalette(dst, 1, 256); } gdImageDestroy(src); } } if (transparent != -1 && colors) { gdImageColorTransparent(dst, gdImageColorExact(dst, red, green, blue)); } sharpen = ngx_http_image_filter_get_value(r, conf->shcv, conf->sharpen); if (sharpen > 0) { gdImageSharpen(dst, sharpen); } gdImageInterlace(dst, (int) conf->interlace); out = ngx_http_image_out(r, ctx->type, dst, &size); ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "image: %d x %d %d", sx, sy, colors); gdImageDestroy(dst); ngx_pfree(r->pool, ctx->image); if (out == NULL) { return NULL; } cln = ngx_pool_cleanup_add(r->pool, 0); if (cln == NULL) { gdFree(out); return NULL; } b = ngx_calloc_buf(r->pool); if (b == NULL) { gdFree(out); return NULL; } cln->handler = ngx_http_image_cleanup; cln->data = out; b->pos = out; b->last = out + size; b->memory = 1; b->last_buf = 1; ngx_http_image_length(r, b); ngx_http_weak_etag(r); return b; } static gdImagePtr ngx_http_image_source(ngx_http_request_t *r, ngx_http_image_filter_ctx_t *ctx) { char *failed; gdImagePtr img; img = NULL; switch (ctx->type) { case NGX_HTTP_IMAGE_JPEG: img = gdImageCreateFromJpegPtr(ctx->length, ctx->image); failed = "gdImageCreateFromJpegPtr() failed"; break; case NGX_HTTP_IMAGE_GIF: img = gdImageCreateFromGifPtr(ctx->length, ctx->image); failed = "gdImageCreateFromGifPtr() failed"; break; case NGX_HTTP_IMAGE_PNG: img = gdImageCreateFromPngPtr(ctx->length, ctx->image); failed = "gdImageCreateFromPngPtr() failed"; break; case NGX_HTTP_IMAGE_WEBP: #if (NGX_HAVE_GD_WEBP) img = gdImageCreateFromWebpPtr(ctx->length, ctx->image); failed = "gdImageCreateFromWebpPtr() failed"; #else failed = "nginx was built without GD WebP support"; #endif break; default: failed = "unknown image type"; break; } if (img == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, failed); } return img; } static gdImagePtr ngx_http_image_new(ngx_http_request_t *r, int w, int h, int colors) { gdImagePtr img; if (colors == 0) { img = gdImageCreateTrueColor(w, h); if (img == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "gdImageCreateTrueColor() failed"); return NULL; } } else { img = gdImageCreate(w, h); if (img == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "gdImageCreate() failed"); return NULL; } } return img; } static u_char * ngx_http_image_out(ngx_http_request_t *r, ngx_uint_t type, gdImagePtr img, int *size) { char *failed; u_char *out; ngx_int_t q; ngx_http_image_filter_conf_t *conf; out = NULL; switch (type) { case NGX_HTTP_IMAGE_JPEG: conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); q = ngx_http_image_filter_get_value(r, conf->jqcv, conf->jpeg_quality); if (q <= 0) { return NULL; } out = gdImageJpegPtr(img, size, q); failed = "gdImageJpegPtr() failed"; break; case NGX_HTTP_IMAGE_GIF: out = gdImageGifPtr(img, size); failed = "gdImageGifPtr() failed"; break; case NGX_HTTP_IMAGE_PNG: out = gdImagePngPtr(img, size); failed = "gdImagePngPtr() failed"; break; case NGX_HTTP_IMAGE_WEBP: #if (NGX_HAVE_GD_WEBP) conf = ngx_http_get_module_loc_conf(r, ngx_http_image_filter_module); q = ngx_http_image_filter_get_value(r, conf->wqcv, conf->webp_quality); if (q <= 0) { return NULL; } out = gdImageWebpPtrEx(img, size, q); failed = "gdImageWebpPtrEx() failed"; #else failed = "nginx was built without GD WebP support"; #endif break; default: failed = "unknown image type"; break; } if (out == NULL) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, failed); } return out; } static void ngx_http_image_cleanup(void *data) { gdFree(data); } static ngx_uint_t ngx_http_image_filter_get_value(ngx_http_request_t *r, ngx_http_complex_value_t *cv, ngx_uint_t v) { ngx_str_t val; if (cv == NULL) { return v; } if (ngx_http_complex_value(r, cv, &val) != NGX_OK) { return 0; } return ngx_http_image_filter_value(&val); } static ngx_uint_t ngx_http_image_filter_value(ngx_str_t *value) { ngx_int_t n; if (value->len == 1 && value->data[0] == '-') { return (ngx_uint_t) -1; } n = ngx_atoi(value->data, value->len); if (n > 0) { return (ngx_uint_t) n; } return 0; } static void * ngx_http_image_filter_create_conf(ngx_conf_t *cf) { ngx_http_image_filter_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_image_filter_conf_t)); if (conf == NULL) { return NULL; } /* * set by ngx_pcalloc(): * * conf->width = 0; * conf->height = 0; * conf->angle = 0; * conf->wcv = NULL; * conf->hcv = NULL; * conf->acv = NULL; * conf->jqcv = NULL; * conf->wqcv = NULL; * conf->shcv = NULL; */ conf->filter = NGX_CONF_UNSET_UINT; conf->jpeg_quality = NGX_CONF_UNSET_UINT; conf->webp_quality = NGX_CONF_UNSET_UINT; conf->sharpen = NGX_CONF_UNSET_UINT; conf->transparency = NGX_CONF_UNSET; conf->interlace = NGX_CONF_UNSET; conf->buffer_size = NGX_CONF_UNSET_SIZE; return conf; } static char * ngx_http_image_filter_merge_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_image_filter_conf_t *prev = parent; ngx_http_image_filter_conf_t *conf = child; if (conf->filter == NGX_CONF_UNSET_UINT) { if (prev->filter == NGX_CONF_UNSET_UINT) { conf->filter = NGX_HTTP_IMAGE_OFF; } else { conf->filter = prev->filter; conf->width = prev->width; conf->height = prev->height; conf->angle = prev->angle; conf->wcv = prev->wcv; conf->hcv = prev->hcv; conf->acv = prev->acv; } } if (conf->jpeg_quality == NGX_CONF_UNSET_UINT) { /* 75 is libjpeg default quality */ ngx_conf_merge_uint_value(conf->jpeg_quality, prev->jpeg_quality, 75); if (conf->jqcv == NULL) { conf->jqcv = prev->jqcv; } } if (conf->webp_quality == NGX_CONF_UNSET_UINT) { /* 80 is libwebp default quality */ ngx_conf_merge_uint_value(conf->webp_quality, prev->webp_quality, 80); if (conf->wqcv == NULL) { conf->wqcv = prev->wqcv; } } if (conf->sharpen == NGX_CONF_UNSET_UINT) { ngx_conf_merge_uint_value(conf->sharpen, prev->sharpen, 0); if (conf->shcv == NULL) { conf->shcv = prev->shcv; } } ngx_conf_merge_value(conf->transparency, prev->transparency, 1); ngx_conf_merge_value(conf->interlace, prev->interlace, 0); ngx_conf_merge_size_value(conf->buffer_size, prev->buffer_size, 1 * 1024 * 1024); return NGX_CONF_OK; } static char * ngx_http_image_filter(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_image_filter_conf_t *imcf = conf; ngx_str_t *value; ngx_int_t n; ngx_uint_t i; ngx_http_complex_value_t cv; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; i = 1; if (cf->args->nelts == 2) { if (ngx_strcmp(value[i].data, "off") == 0) { imcf->filter = NGX_HTTP_IMAGE_OFF; } else if (ngx_strcmp(value[i].data, "test") == 0) { imcf->filter = NGX_HTTP_IMAGE_TEST; } else if (ngx_strcmp(value[i].data, "size") == 0) { imcf->filter = NGX_HTTP_IMAGE_SIZE; } else { goto failed; } return NGX_CONF_OK; } else if (cf->args->nelts == 3) { if (ngx_strcmp(value[i].data, "rotate") == 0) { if (imcf->filter != NGX_HTTP_IMAGE_RESIZE && imcf->filter != NGX_HTTP_IMAGE_CROP) { imcf->filter = NGX_HTTP_IMAGE_ROTATE; } ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[++i]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[i]); if (n != 90 && n != 180 && n != 270) { goto failed; } imcf->angle = (ngx_uint_t) n; } else { imcf->acv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->acv == NULL) { return NGX_CONF_ERROR; } *imcf->acv = cv; } return NGX_CONF_OK; } else { goto failed; } } if (ngx_strcmp(value[i].data, "resize") == 0) { imcf->filter = NGX_HTTP_IMAGE_RESIZE; } else if (ngx_strcmp(value[i].data, "crop") == 0) { imcf->filter = NGX_HTTP_IMAGE_CROP; } else { goto failed; } ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[++i]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[i]); if (n == 0) { goto failed; } imcf->width = (ngx_uint_t) n; } else { imcf->wcv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->wcv == NULL) { return NGX_CONF_ERROR; } *imcf->wcv = cv; } ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[++i]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[i]); if (n == 0) { goto failed; } imcf->height = (ngx_uint_t) n; } else { imcf->hcv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->hcv == NULL) { return NGX_CONF_ERROR; } *imcf->hcv = cv; } return NGX_CONF_OK; failed: ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid parameter \"%V\"", &value[i]); return NGX_CONF_ERROR; } static char * ngx_http_image_filter_jpeg_quality(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_image_filter_conf_t *imcf = conf; ngx_str_t *value; ngx_int_t n; ngx_http_complex_value_t cv; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[1]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[1]); if (n <= 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value \"%V\"", &value[1]); return NGX_CONF_ERROR; } imcf->jpeg_quality = (ngx_uint_t) n; } else { imcf->jqcv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->jqcv == NULL) { return NGX_CONF_ERROR; } *imcf->jqcv = cv; } return NGX_CONF_OK; } static char * ngx_http_image_filter_webp_quality(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_image_filter_conf_t *imcf = conf; ngx_str_t *value; ngx_int_t n; ngx_http_complex_value_t cv; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[1]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[1]); if (n <= 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value \"%V\"", &value[1]); return NGX_CONF_ERROR; } imcf->webp_quality = (ngx_uint_t) n; } else { imcf->wqcv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->wqcv == NULL) { return NGX_CONF_ERROR; } *imcf->wqcv = cv; } return NGX_CONF_OK; } static char * ngx_http_image_filter_sharpen(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_image_filter_conf_t *imcf = conf; ngx_str_t *value; ngx_int_t n; ngx_http_complex_value_t cv; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[1]; ccv.complex_value = &cv; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; } if (cv.lengths == NULL) { n = ngx_http_image_filter_value(&value[1]); if (n < 0) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "invalid value \"%V\"", &value[1]); return NGX_CONF_ERROR; } imcf->sharpen = (ngx_uint_t) n; } else { imcf->shcv = ngx_palloc(cf->pool, sizeof(ngx_http_complex_value_t)); if (imcf->shcv == NULL) { return NGX_CONF_ERROR; } *imcf->shcv = cv; } return NGX_CONF_OK; } static ngx_int_t ngx_http_image_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_image_header_filter; ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_image_body_filter; return NGX_OK; }