diff --git a/hwe/sql/schema.sql b/hwe/sql/schema.sql
index 6eeefdfb..d8f39ad1 100644
--- a/hwe/sql/schema.sql
+++ b/hwe/sql/schema.sql
@@ -11,7 +11,7 @@ CREATE TABLE `general` (
`bornyear` INT(3) NULL DEFAULT '180',
`deadyear` INT(3) NULL DEFAULT '300',
`newmsg` INT(1) NULL DEFAULT '0',
- `picture` VARCHAR(40) NOT NULL,
+ `picture` VARCHAR(64) NOT NULL,
`imgsvr` INT(1) NOT NULL DEFAULT '0',
`name` VARCHAR(32) NOT NULL COLLATE 'utf8mb4_bin',
`owner_name` VARCHAR(32) NULL DEFAULT NULL COLLATE 'utf8mb4_bin',
diff --git a/scripts/.htaccess b/scripts/.htaccess
new file mode 100644
index 00000000..a4738cfd
--- /dev/null
+++ b/scripts/.htaccess
@@ -0,0 +1,6 @@
+
+ Require all denied
+
+
+ Deny from all
+
diff --git a/scripts/migrate-general-picture.php b/scripts/migrate-general-picture.php
new file mode 100755
index 00000000..af19ceef
--- /dev/null
+++ b/scripts/migrate-general-picture.php
@@ -0,0 +1,111 @@
+#!/usr/bin/env php
+queryFirstRow('SHOW COLUMNS FROM general WHERE Field = %s', 'picture');
+ if (!$column || !is_string($column['Type'] ?? null)) {
+ return null;
+ }
+ if (preg_match('/^varchar\((\d+)\)$/i', $column['Type'], $matches) !== 1) {
+ return null;
+ }
+ return (int)$matches[1];
+}
+
+function pictureMigrationState(\MeekroDB $db): string
+{
+ $capacity = pictureColumnCapacity($db);
+ if ($capacity === 40) {
+ return 'legacy';
+ }
+ if ($capacity !== null && $capacity >= 64) {
+ return 'ready';
+ }
+ return 'unsupported';
+}
+
+function printPictureMigrationStatus(\MeekroDB $db): string
+{
+ $state = pictureMigrationState($db);
+ $capacity = pictureColumnCapacity($db);
+ printf("schema_state=%s\npicture_capacity=%s\n", $state, $capacity ?? 'unknown');
+ return $state;
+}
+
+$options = getopt('', ['help', 'status', 'apply', 'backup:']);
+if (isset($options['help'])) {
+ pictureMigrationUsage();
+}
+if (isset($options['status']) === isset($options['apply'])) {
+ pictureMigrationUsage(2);
+}
+
+$db = DB::db();
+if (isset($options['status'])) {
+ exit(printPictureMigrationStatus($db) === 'unsupported' ? 2 : 0);
+}
+
+$state = pictureMigrationState($db);
+if ($state === 'ready') {
+ fwrite(STDOUT, "general.picture is already VARCHAR(64) or wider; nothing to do.\n");
+ exit(0);
+}
+if ($state !== 'legacy') {
+ fwrite(STDERR, "general.picture is not the supported VARCHAR(40) schema; inspect --status first.\n");
+ exit(2);
+}
+
+$backup = $options['backup'] ?? null;
+if (!is_string($backup) || $backup === '' || $backup[0] !== '/' || !is_file($backup) || filesize($backup) === 0) {
+ fwrite(STDERR, "--backup must name a pre-existing, non-empty absolute SQL backup made immediately before migration.\n");
+ exit(2);
+}
+if (!\sammo\tryLock()) {
+ fwrite(STDERR, "Unable to acquire the GAME lock.\n");
+ exit(3);
+}
+
+try {
+ $db->query('ALTER TABLE general MODIFY picture VARCHAR(64) NOT NULL');
+} finally {
+ \sammo\unlock();
+}
+
+if (printPictureMigrationStatus($db) !== 'ready') {
+ fwrite(STDERR, "general.picture migration verification failed; restore the supplied backup.\n");
+ exit(4);
+}
+fwrite(STDOUT, "general.picture migration completed.\n");
diff --git a/tests/GeneralPictureSchemaTest.php b/tests/GeneralPictureSchemaTest.php
new file mode 100644
index 00000000..c34133cd
--- /dev/null
+++ b/tests/GeneralPictureSchemaTest.php
@@ -0,0 +1,42 @@
+