Commit d7778413a2a for php.net
commit d7778413a2aae6ed758fe282e676460a405c0fa0
Author: Lazizbek Ergashev <lazerg2@gmail.com>
Date: Tue Aug 25 23:21:28 2026 +0500
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
diff --git a/NEWS b/NEWS
index e56e562a4c9..c6ca2040030 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 c1258652259..92001c4f996 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 00000000000..77a3a61900d
--- /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--
+<?php
+class write_counter
+{
+ public $context;
+
+ public static int $writes = 0;
+
+ public function stream_open(string $path, string $mode, int $options, ?string &$opened_path): bool
+ {
+ return true;
+ }
+
+ public function stream_write(string $data): int
+ {
+ self::$writes++;
+ return strlen($data);
+ }
+
+ public function stream_close(): void
+ {
+ }
+}
+
+stream_wrapper_register('gh23457', write_counter::class);
+
+$im = imagecreatetruecolor(200, 200);
+var_dump(imagebmp($im, 'gh23457://image.bmp'));
+var_dump(write_counter::$writes < 100);
+?>
+--EXPECT--
+bool(true)
+bool(true)