From d7778413a2aae6ed758fe282e676460a405c0fa0 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Tue, 25 Aug 2026 23:21:28 +0500 Subject: [PATCH] Fix GH-23457: imagebmp() is extremely slow when writing to a file imagebmp() writes its pixel data a byte at a time, and the gd stream context turned each of those bytes into its own php_stream_write() call. PHP streams do no write buffering, so a 1920x1080 truecolor image cost about six million write syscalls. libgd's own FILE context does not show this because stdio buffers for it. Buffering the stream context in 8 KB chunks takes that image from 9.5s to 0.02s here, with byte-identical output. imagewbmp(), imagegd() and imagegd2() go through the same context and were writing per byte too, so they get the same fix. imagexbm() goes through the same context but writes its output via putBuf rather than per-byte putC, so it was not affected by this bug and sees no change from this patch. Close GH-23460 --- NEWS | 2 ++ ext/gd/gd.c | 28 ++++++++++++++++++++++++---- ext/gd/tests/gh23457.phpt | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 ext/gd/tests/gh23457.phpt diff --git a/NEWS b/NEWS index e56e562a4c92..c6ca2040030d 100644 --- a/NEWS +++ b/NEWS @@ -23,6 +23,8 @@ PHP NEWS - GD: . Fixed imageaffinematrixget() and imageaffinematrixconcat() reporting the wrong argument in error messages. (Weilin Du) + . Fixed bug GH-23457 (imagebmp() is extremely slow when writing to a file). + (Lazizbek Ergashev) - Intl: . Fixed a double-free when IntlGregorianCalendar construction fails after diff --git a/ext/gd/gd.c b/ext/gd/gd.c index c12586522594..92001c4f9967 100644 --- a/ext/gd/gd.c +++ b/ext/gd/gd.c @@ -4464,21 +4464,39 @@ static void _php_image_output_ctxfree(struct gdIOCtx *ctx) /* {{{ */ efree(ctx); } /* }}} */ +typedef struct { + gdIOCtx ctx; + size_t buf_len; + unsigned char buf[8192]; +} php_gd_stream_ctx; + +static void _php_image_stream_flush(php_gd_stream_ctx *stream_ctx) /* {{{ */ +{ + if (stream_ctx->buf_len) { + php_stream_write((php_stream *) stream_ctx->ctx.data, (char *) stream_ctx->buf, stream_ctx->buf_len); + stream_ctx->buf_len = 0; + } +} /* }}} */ + static void _php_image_stream_putc(struct gdIOCtx *ctx, int c) /* {{{ */ { - char ch = (char) c; - php_stream * stream = (php_stream *)ctx->data; - php_stream_write(stream, &ch, 1); + php_gd_stream_ctx *stream_ctx = (php_gd_stream_ctx *) ctx; + if (stream_ctx->buf_len == sizeof(stream_ctx->buf)) { + _php_image_stream_flush(stream_ctx); + } + stream_ctx->buf[stream_ctx->buf_len++] = (unsigned char) c; } /* }}} */ static int _php_image_stream_putbuf(struct gdIOCtx *ctx, const void* buf, int l) /* {{{ */ { php_stream * stream = (php_stream *)ctx->data; + _php_image_stream_flush((php_gd_stream_ctx *) ctx); return php_stream_write(stream, (void *)buf, l); } /* }}} */ static void _php_image_stream_ctxfree(struct gdIOCtx *ctx) /* {{{ */ { if(ctx->data) { + _php_image_stream_flush((php_gd_stream_ctx *) ctx); ctx->data = NULL; } efree(ctx); @@ -4487,6 +4505,7 @@ static void _php_image_stream_ctxfree(struct gdIOCtx *ctx) /* {{{ */ static void _php_image_stream_ctxfreeandclose(struct gdIOCtx *ctx) /* {{{ */ { if(ctx->data) { + _php_image_stream_flush((php_gd_stream_ctx *) ctx); php_stream_close((php_stream *) ctx->data); ctx->data = NULL; } @@ -4494,7 +4513,8 @@ static void _php_image_stream_ctxfreeandclose(struct gdIOCtx *ctx) /* {{{ */ } /* }}} */ static gdIOCtx *create_stream_context(php_stream *stream, int close_stream) { - gdIOCtx *ctx = ecalloc(1, sizeof(gdIOCtx)); + php_gd_stream_ctx *stream_ctx = ecalloc(1, sizeof(php_gd_stream_ctx)); + gdIOCtx *ctx = &stream_ctx->ctx; ctx->putC = _php_image_stream_putc; ctx->putBuf = _php_image_stream_putbuf; diff --git a/ext/gd/tests/gh23457.phpt b/ext/gd/tests/gh23457.phpt new file mode 100644 index 000000000000..77a3a61900d1 --- /dev/null +++ b/ext/gd/tests/gh23457.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-23457 (imagebmp() writes to the stream one byte at a time) +--EXTENSIONS-- +gd +--FILE-- + +--EXPECT-- +bool(true) +bool(true)