[php-src] PHP-8.4: Fix GH-23457: imagebmp() is extremely slow when writing to a file

Lazizbek Ergashev via David Carlier <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Lazizbek Ergashev (lazerg)
Committer: David Carlier (devnexen)
Date: 2026-08-26T12:33:47+01:00

Commit: https://github.com/php/php-src/commit/d7778413a2aae6ed758fe282e676460a405c0fa0
Raw diff: https://github.com/php/php-src/commit/d7778413a2aae6ed758fe282e676460a405c0fa0.diff

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

Changed paths:
  A  ext/gd/tests/gh23457.phpt
  M  NEWS
  M  ext/gd/gd.c


Diff:

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--
+<?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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.