Plugin Directory

Changeset 3757771


Ignore:
Timestamp:
10/21/2026 06:09:32 AM (5 days ago)
Author:
warnz
Message:

Release 1.0.1: server-friendly resource management

Location:
bytcrush-image-compression
Files:
2 added
2 deleted
22 edited
1 copied

Legend:

Unmodified
Added
Removed
  • bytcrush-image-compression/tags/1.0.1/assets/js/admin.js

    r3753311 r3757771  
    321321                                                return;
    322322                                        }
     323                                        if ( res.cooldown > 0 ) {
     324                                                // The server is overloaded: give it a break, then carry on.
     325                                                sub.textContent = i18n.cooling;
     326                                                for ( var waited = 0; waited < res.cooldown && run.active; waited++ ) {
     327                                                        await sleep( 1000 );
     328                                                }
     329                                                if ( run.active ) {
     330                                                        sub.textContent = i18n.subRunning;
     331                                                }
     332                                        }
    323333                                } catch ( err ) {
    324334                                        failures++;
  • bytcrush-image-compression/tags/1.0.1/assets/js/background.js

    r3753311 r3757771  
    8989                                }
    9090
    91                                 if ( res.stalled ) {
     91                                if ( res.cooldown > 0 ) {
     92                                        // Server overloaded: the run is backing off and resumes by itself.
     93                                        d.setSub( t.subCooling );
     94                                        await sleep( Math.min( res.cooldown, 10 ) * 1000 );
     95                                } else if ( res.stalled ) {
    9296                                        // Loopback blocked or chain broken: this tab does the work.
    9397                                        d.setSub( t.subFallback );
  • bytcrush-image-compression/tags/1.0.1/bytcrush-image-compression.php

    r3753311 r3757771  
    55 * Plugin URI:        https://warcal.dev/wordpress/plugins/bytcrush-image-compression
    66 * Description:       Optimize every image in your Media Library in one click with the BytCrush compression engine. Originals are always kept safe, and WebP/AVIF versions are served automatically.
    7  * Version:           1.0.0
     7 * Version:           1.0.1
    88 * Requires at least: 5.3
    99 * Requires PHP:      7.2
    … …  
    4141 * ---------------------------------------------------------------------------
    4242 */
    43 define( 'BYTC_VERSION', '1.0.0' );
     43define( 'BYTC_VERSION', '1.0.1' );
    4444define( 'BYTC_FILE', __FILE__ );
    4545define( 'BYTC_DIR', plugin_dir_path( __FILE__ ) );
  • bytcrush-image-compression/tags/1.0.1/includes/Admin/Admin.php

    r3753311 r3757771  
    157157                'subBackground' => __( 'Running on your server. You can close this tab and come back any time.', 'bytcrush-image-compression' ),
    158158                'subFallback'   => __( 'Your server is blocking background requests, so this tab is doing the work. Keep it open to continue.', 'bytcrush-image-compression' ),
     159                'subCooling'    => __( 'Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically.', 'bytcrush-image-compression' ),
    159160            ),
    160161        ) );
    … …  
    230231            'unsaved'           => __( 'You have unsaved changes', 'bytcrush-image-compression' ),
    231232            'saveFailed'        => __( 'Could not save settings', 'bytcrush-image-compression' ),
     233            'cooling'           => __( 'Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically.', 'bytcrush-image-compression' ),
    232234            'leaveWarning'      => __( 'BytCrush is still optimizing. Leaving will pause the run.', 'bytcrush-image-compression' ),
    233235        );
  • bytcrush-image-compression/tags/1.0.1/includes/Engine/Background.php

    r3753311 r3757771  
    101101                                'halted'    => '',
    102102                                'finished'  => 0,
     103                                'resume_at' => 0,
    103104                        )
    104105                );
    … …  
    128129                                        'halted'    => '',
    129130                                        'finished'  => 0,
     131                                        'resume_at' => 0,
    130132                                )
    131133                        );
    … …  
    216218                        return;
    217219                }
    218                 if ( self::stalled() ) {
     220                $state = self::state();
     221                if ( self::stalled() || ( $state['resume_at'] && ! self::cooling() && ! Lock::since( self::LOCK ) ) ) {
    219222                        self::work( true );
    220223                }
    … …  
    227230         */
    228231        public static function stalled() {
     232                if ( self::cooling() ) {
     233                        return false; // Deliberate back-off, not a broken chain.
     234                }
    229235                $locked = Lock::since( self::LOCK );
    230236                if ( $locked && ( time() - $locked ) < 5 * MINUTE_IN_SECONDS ) {
    … …  
    241247         */
    242248        public static function work( $chain ) {
    243                 if ( ! self::running() || ! Lock::acquire( self::LOCK, 5 * MINUTE_IN_SECONDS ) ) {
     249                // Cooling down after an overloaded batch: the cron job resumes the run.
     250                if ( ! self::running() || self::cooling() || ! Lock::acquire( self::LOCK, 5 * MINUTE_IN_SECONDS ) ) {
    244251                        return array();
    245252                }
    246253
    247                 $items  = array();
    248                 $halted = '';
     254                $items    = array();
     255                $halted   = '';
     256                $cooldown = 0;
    249257
    250258                try {
    251259                        self::beat();
    252                         $budget = (float) apply_filters( 'bytcrush_bulk_time_budget', 20 );
    253                         $start  = microtime( true );
    254260                        $seen   = array();
    255261                        $streak = 0;
    256262
    257                         do {
    258                                 if ( ! self::running() ) {
    259                                         break;
    260                                 }
    261                                 $ids = Stats::pending_ids( 1, $seen );
    262                                 if ( ! $ids ) {
    263                                         break;
    264                                 }
    265                                 $seen[]  = $ids[0];
    266                                 $result  = Optimizer::optimize( $ids[0] );
    267                                 $items[] = self::record( Controller::item( $ids[0], $result ) );
    268 
    269                                 $streak = 'error' === $result['status'] ? $streak + 1 : 0;
    270                                 if ( $streak >= 10 ) {
    271                                         $halted = $result['message'];
    272                                         break;
    273                                 }
    274                         } while ( microtime( true ) - $start < $budget );
     263                        // Time-boxed to the host's limits and paced by server load.
     264                        $batch    = Resources::batch(
     265                                static function () use ( &$seen ) {
     266                                        $ids = Stats::pending_ids( 1, $seen );
     267                                        return $ids ? $ids[0] : null;
     268                                },
     269                                static function ( $id ) use ( &$seen, &$items, &$streak, &$halted ) {
     270                                        $seen[]  = $id;
     271                                        $result  = Optimizer::optimize( $id );
     272                                        $items[] = self::record( Controller::item( $id, $result ) );
     273
     274                                        $streak = 'error' === $result['status'] ? $streak + 1 : 0;
     275                                        if ( $streak >= 10 ) {
     276                                                $halted = $result['message'];
     277                                                return true;
     278                                        }
     279                                        return false;
     280                                },
     281                                array( __CLASS__, 'running' )
     282                        );
     283                        $cooldown = $batch['cooldown'];
    275284                } finally {
    276285                        Lock::release( self::LOCK );
    … …  
    281290                } elseif ( ! Stats::pending_ids( 1 ) ) {
    282291                        self::stop();
     292                } elseif ( $cooldown ) {
     293                        // Server overloaded: back off; cron (or the open tab) resumes after.
     294                        $state              = self::state();
     295                        $state['resume_at'] = time() + $cooldown;
     296                        $state['beat']      = time();
     297                        self::save_state( $state );
    283298                } elseif ( $chain && self::running() ) {
    284299                        self::dispatch();
    … …  
    286301
    287302                return $items;
     303        }
     304
     305        /**
     306         * Whether the run is backing off because the server was overloaded.
     307         *
     308         * @return bool
     309         */
     310        public static function cooling() {
     311                return (int) self::state()['resume_at'] > time();
    288312        }
    289313
    … …  
    301325                        'running'   => $running,
    302326                        'stalled'   => $running && self::stalled(),
     327                        'cooldown'  => $running ? max( 0, (int) $state['resume_at'] - time() ) : 0,
    303328                        'processed' => (int) $state['processed'],
    304329                        'elapsed'   => $state['started'] ? max( 1, time() - (int) $state['started'] ) : 0,
  • bytcrush-image-compression/tags/1.0.1/includes/Engine/Compressor.php

    r3753311 r3757771  
    164164                                sprintf( __( 'This server cannot create %s images.', 'bytcrush-image-compression' ), strtoupper( $format ) )
    165165                        );
     166                }
     167
     168                // Check memory before decoding: GD holds every pixel in PHP memory, and a
     169                // huge image would kill the request. Imagick works outside PHP's limit
     170                // (with its own capped budget that spills to disk), so hand it over;
     171                // with no alternative, skip the image instead of crashing.
     172                if ( GdDriver::class === $class ) {
     173                        $size = @getimagesize( $src ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- header read only.
     174                        if ( $size ) {
     175                                list( $target_w, $target_h ) = self::fitted_size( (int) $size[0], (int) $size[1], (int) $o['max_width'], (int) $o['max_height'] );
     176                                $needed                      = Resources::gd_memory_needed( (int) $size[0], (int) $size[1], $target_w, $target_h );
     177                                $available                   = Resources::memory_available();
     178                                if ( $needed > $available ) {
     179                                        if ( ImagickDriver::available() && ImagickDriver::can_encode( $format ) ) {
     180                                                $class = ImagickDriver::class;
     181                                        } else {
     182                                                return new \WP_Error(
     183                                                        'bytcrush_too_large',
     184                                                        sprintf(
     185                                                                /* translators: 1: image size in megapixels, 2: memory needed, 3: memory available. */
     186                                                                __( 'Skipped to keep your site safe: this %1$s MP image needs about %2$s of memory and the server allows %3$s.', 'bytcrush-image-compression' ),
     187                                                                number_format_i18n( $size[0] * $size[1] / 1000000, 1 ),
     188                                                                size_format( $needed ),
     189                                                                size_format( $available )
     190                                                        )
     191                                                );
     192                                        }
     193                                }
     194                        }
    166195                }
    167196
    … …  
    270299         */
    271300        private static function fit( Driver $driver, $max_width, $max_height ) {
    272                 if ( $max_width <= 0 && $max_height <= 0 ) {
     301                list( $width, $height ) = self::fitted_size( $driver->width(), $driver->height(), $max_width, $max_height );
     302                if ( $width === $driver->width() && $height === $driver->height() ) {
    273303                        return false;
    274304                }
    275                 $w     = $driver->width();
    276                 $h     = $driver->height();
     305                $driver->resize( $width, $height );
     306                return true;
     307        }
     308
     309        /**
     310         * Dimensions after fitting inside a box (unchanged when already inside).
     311         *
     312         * @param int $w          Width.
     313         * @param int $h          Height.
     314         * @param int $max_width  Max width (0 = unbounded).
     315         * @param int $max_height Max height (0 = unbounded).
     316         * @return int[] { width, height }
     317         */
     318        private static function fitted_size( $w, $h, $max_width, $max_height ) {
     319                if ( ( $max_width <= 0 && $max_height <= 0 ) || $w < 1 || $h < 1 ) {
     320                        return array( $w, $h );
     321                }
    277322                $box_w = $max_width > 0 ? $max_width : $w;
    278323                $box_h = $max_height > 0 ? $max_height : $h;
    279324                if ( $w <= $box_w && $h <= $box_h ) {
    280                         return false;
     325                        return array( $w, $h );
    281326                }
    282327                $ratio = min( $box_w / $w, $box_h / $h );
    283                 $driver->resize( max( 1, (int) round( $w * $ratio ) ), max( 1, (int) round( $h * $ratio ) ) );
    284                 return true;
     328                return array( max( 1, (int) round( $w * $ratio ) ), max( 1, (int) round( $h * $ratio ) ) );
    285329        }
    286330
  • bytcrush-image-compression/tags/1.0.1/includes/Engine/Driver/ImagickDriver.php

    r3753311 r3757771  
    5454                }
    5555
     56                self::limit_resources();
    5657                $im = new \Imagick();
    5758                $im->readImage( $path );
    … …  
    6364
    6465                $this->im = $im;
     66        }
     67
     68        /**
     69         * Keep ImageMagick within what the server can spare. Its memory is not
     70         * counted against PHP's memory_limit and it uses every CPU core by default,
     71         * which on shared hosting can get the process throttled or killed. Cap it
     72         * to one thread (as WordPress core does) and to a memory budget that
     73         * matches PHP's; above that, its pixel cache spills to disk instead.
     74         *
     75         * @return void
     76         */
     77        private static function limit_resources() {
     78                static $done = false;
     79                if ( $done || ! method_exists( '\Imagick', 'setResourceLimit' ) ) {
     80                        return;
     81                }
     82                $done = true;
     83
     84                /**
     85                 * Threads ImageMagick may use per image (0 = leave its default).
     86                 *
     87                 * @param int $threads Threads.
     88                 */
     89                $threads = (int) apply_filters( 'bytcrush_imagick_threads', 1 );
     90                if ( $threads > 0 && defined( '\Imagick::RESOURCETYPE_THREAD' ) ) {
     91                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_THREAD, $threads );
     92                }
     93
     94                $memory = (int) min( 256 * MB_IN_BYTES, max( 32 * MB_IN_BYTES, \BytCrush\Engine\Resources::memory_available() ) );
     95                if ( defined( '\Imagick::RESOURCETYPE_MEMORY' ) ) {
     96                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_MEMORY, $memory );
     97                }
     98                if ( defined( '\Imagick::RESOURCETYPE_MAP' ) ) {
     99                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_MAP, $memory * 2 );
     100                }
    65101        }
    66102
  • bytcrush-image-compression/tags/1.0.1/includes/Engine/Optimizer.php

    r3753311 r3757771  
    277277
    278278                                if ( is_wp_error( $res ) ) {
    279                                         // Animated GIF/PNG: skip the image (as the WebP step does), don't flag an error.
    280                                         if ( 'bytcrush_animated' === $res->get_error_code() ) {
     279                                        // Animated, or too large for this server's memory: skip the image with the reason, don't flag an error.
     280                                        if ( in_array( $res->get_error_code(), array( 'bytcrush_animated', 'bytcrush_too_large' ), true ) ) {
    281281                                                return self::finish( $id, 'skipped', $res->get_error_message(), $meta );
    282282                                        }
    … …  
    341341
    342342                                if ( is_wp_error( $bytes ) ) {
    343                                         if ( 'bytcrush_animated' === $bytes->get_error_code() ) {
     343                                        if ( in_array( $bytes->get_error_code(), array( 'bytcrush_animated', 'bytcrush_too_large' ), true ) ) {
    344344                                                return self::finish( $id, 'skipped', $bytes->get_error_message(), $meta );
    345345                                        }
  • bytcrush-image-compression/tags/1.0.1/includes/Rest/Controller.php

    r3753311 r3757771  
    1818use BytCrush\Engine\Backup;
    1919use BytCrush\Engine\Optimizer;
     20use BytCrush\Engine\Resources;
    2021use BytCrush\Engine\Stats;
    2122use WP_REST_Request;
    … …  
    164165                bytcrush_keep_running();
    165166
    166                 /**
    167                  * Seconds of work per bulk request. Keep well under the host's
    168                  * max_execution_time / proxy timeout.
    169                  *
    170                  * @param float $seconds Budget.
    171                  */
    172                 $budget = (float) apply_filters( 'bytcrush_bulk_time_budget', 20 );
    173                 $start  = microtime( true );
    174167                $items  = array();
    175168                $seen   = array();
    … …  
    177170                $halted = '';
    178171
    179                 do {
    180                         $ids = Stats::pending_ids( 1, $seen );
    181                         if ( ! $ids ) {
    182                                 break;
     172                // Time-boxed to the host's limits and paced by server load.
     173                $batch = Resources::batch(
     174                        static function () use ( &$seen ) {
     175                                $ids = Stats::pending_ids( 1, $seen );
     176                                return $ids ? $ids[0] : null;
     177                        },
     178                        static function ( $id ) use ( &$seen, &$items, &$streak, &$halted ) {
     179                                $seen[]  = $id;
     180                                $result  = Optimizer::optimize( $id );
     181                                $items[] = self::item( $id, $result );
     182
     183                                // A run of consecutive failures means a site-wide problem (disk
     184                                // full, permissions): stop instead of failing the whole library.
     185                                $streak = 'error' === $result['status'] ? $streak + 1 : 0;
     186                                if ( $streak >= 10 ) {
     187                                        $halted = $result['message'];
     188                                        return true;
     189                                }
     190                                return false;
    183191                        }
    184                         $seen[]  = $ids[0];
    185                         $result  = Optimizer::optimize( $ids[0] );
    186                         $items[] = self::item( $ids[0], $result );
    187 
    188                         // A run of consecutive failures means a site-wide problem (disk
    189                         // full, permissions): stop instead of failing the whole library.
    190                         $streak = 'error' === $result['status'] ? $streak + 1 : 0;
    191                         if ( $streak >= 10 ) {
    192                                 $halted = $result['message'];
    193                                 break;
    194                         }
    195                 } while ( microtime( true ) - $start < $budget );
    196 
    197                 return rest_ensure_response(
    198                         array(
    199                                 'items'  => $items,
    200                                 'stats'  => Stats::get(),
    201                                 'done'   => ! Stats::pending_ids( 1 ),
    202                                 'halted' => $halted,
     192                );
     193
     194                return rest_ensure_response(
     195                        array(
     196                                'items'    => $items,
     197                                'stats'    => Stats::get(),
     198                                'done'     => ! Stats::pending_ids( 1 ),
     199                                'halted'   => $halted,
     200                                'cooldown' => $batch['cooldown'],
    203201                        )
    204202                );
  • bytcrush-image-compression/tags/1.0.1/languages/bytcrush-image-compression.pot

    r3753311 r3757771  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: BytCrush Image Compression 1.0.0\n"
     5"Project-Id-Version: BytCrush Image Compression 1.0.1\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/app\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-14T15:53:40+00:00\n"
     12"POT-Creation-Date: 2026-10-21T04:14:25+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1717#. Plugin Name of the plugin
    1818#: bytcrush-image-compression.php
    19 #: includes/Admin/Admin.php:61
     19#: includes/Admin/Admin.php:62
    2020#: views/dashboard.php:46
    2121msgid "BytCrush Image Compression"
    … …  
    4848msgstr ""
    4949
    50 #: includes/Admin/Admin.php:62
     50#: includes/Admin/Admin.php:63
    5151#: includes/Admin/MediaColumn.php:38
    5252#: includes/Admin/MediaColumn.php:65
    … …  
    5454msgstr ""
    5555
    56 #: includes/Admin/Admin.php:68
     56#: includes/Admin/Admin.php:69
    5757#: views/header.php:15
    5858msgid "Dashboard"
    5959msgstr ""
    6060
    61 #: includes/Admin/Admin.php:71
     61#: includes/Admin/Admin.php:72
    6262#: views/header.php:20
    6363msgid "Page audit"
    6464msgstr ""
    6565
    66 #: includes/Admin/Admin.php:73
    67 #: includes/Admin/Admin.php:292
     66#: includes/Admin/Admin.php:74
     67#: includes/Admin/Admin.php:301
    6868#: views/header.php:16
    6969msgid "Settings"
    7070msgstr ""
    7171
    72 #: includes/Admin/Admin.php:76
     72#: includes/Admin/Admin.php:77
    7373#: views/pro.php:33
    7474msgid "BytCrush Pro"
    7575msgstr ""
    7676
    77 #: includes/Admin/Admin.php:76
    78 #: includes/Admin/Admin.php:295
     77#: includes/Admin/Admin.php:77
     78#: includes/Admin/Admin.php:304
    7979#: views/header.php:23
    8080msgid "Go Pro"
    8181msgstr ""
    8282
    83 #: includes/Admin/Admin.php:133
     83#: includes/Admin/Admin.php:135
    8484msgid "Running on your server. You can close this tab and come back any time."
    8585msgstr ""
    8686
    87 #: includes/Admin/Admin.php:134
     87#: includes/Admin/Admin.php:136
    8888msgid "Your server is blocking background requests, so this tab is doing the work. Keep it open to continue."
    8989msgstr ""
    9090
    91 #. translators: %s: number of images.
     91#: includes/Admin/Admin.php:137
     92#: includes/Admin/Admin.php:201
     93msgid "Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically."
     94msgstr ""
     95
    9296#: includes/Admin/Admin.php:179
    93 #: views/dashboard.php:34
    94 #, php-format
    95 msgid "Optimize %s images in one click"
     97msgid "Crushing bytes…"
    9698msgstr ""
    9799
    98100#: includes/Admin/Admin.php:180
    99 msgid "Crushing bytes…"
     101msgid "Paused"
    100102msgstr ""
    101103
    102104#: includes/Admin/Admin.php:181
    103 msgid "Paused"
    104 msgstr ""
    105 
    106 #: includes/Admin/Admin.php:182
    107105#: views/dashboard.php:35
    108106msgid "Your library is fully optimized"
    109107msgstr ""
    110108
    111 #. translators: %s: number of images.
    112 #: includes/Admin/Admin.php:184
    113 #: views/dashboard.php:32
    114 #, php-format
    115 msgid "%s images could not be optimized"
    116 msgstr ""
    117 
    118 #: includes/Admin/Admin.php:185
     109#: includes/Admin/Admin.php:182
    119110#: views/dashboard.php:39
    120111msgid "The reasons are listed below. Fix the cause, then press Retry."
    121112msgstr ""
    122113
     114#: includes/Admin/Admin.php:183
     115msgid "Paused after 10 failures in a row. "
     116msgstr ""
     117
     118#: includes/Admin/Admin.php:184
     119msgid "Restoring originals…"
     120msgstr ""
     121
     122#: includes/Admin/Admin.php:185
     123msgid "Keep this tab open. You can pause at any time and pick up where you left off."
     124msgstr ""
     125
    123126#: includes/Admin/Admin.php:186
    124 msgid "Paused after 10 failures in a row. "
    125 msgstr ""
    126 
    127 #: includes/Admin/Admin.php:187
    128 msgid "Restoring originals…"
    129 msgstr ""
    130 
    131 #: includes/Admin/Admin.php:188
    132 msgid "Keep this tab open. You can pause at any time and pick up where you left off."
    133 msgstr ""
    134 
    135 #. translators: %s: number of images.
    136 #: includes/Admin/Admin.php:190
    137 #, php-format
    138 msgid "%s images left. Resume whenever you are ready."
    139 msgstr ""
    140 
    141 #: includes/Admin/Admin.php:191
    142127#: views/dashboard.php:41
    143128msgid "New uploads are optimized automatically. Originals are safe in backups."
    144129msgstr ""
    145130
    146 #: includes/Admin/Admin.php:192
     131#: includes/Admin/Admin.php:187
    147132#: views/dashboard.php:93
    148133msgid "Optimize all images"
    149134msgstr ""
    150135
    151 #: includes/Admin/Admin.php:193
     136#: includes/Admin/Admin.php:188
    152137msgid "Pause"
    153138msgstr ""
    154139
    155 #: includes/Admin/Admin.php:194
     140#: includes/Admin/Admin.php:189
    156141msgid "Resume"
    157142msgstr ""
    158143
    159 #: includes/Admin/Admin.php:195
     144#: includes/Admin/Admin.php:190
    160145#: views/dashboard.php:93
    161146msgid "All images optimized"
    162147msgstr ""
    163148
    164 #. translators: 1: processed count, 2: total count.
    165 #: includes/Admin/Admin.php:197
    166 #, php-format
    167 msgid "%1$s of %2$s images"
    168 msgstr ""
    169 
    170 #. translators: %s: time, e.g. 2m 10s.
    171 #: includes/Admin/Admin.php:199
    172 #, php-format
    173 msgid "about %s left"
    174 msgstr ""
    175 
    176 #: includes/Admin/Admin.php:200
     149#: includes/Admin/Admin.php:191
    177150#: includes/Admin/MediaColumn.php:128
    178151#: views/dashboard.php:220
    … …  
    180153msgstr ""
    181154
    182 #: includes/Admin/Admin.php:201
     155#: includes/Admin/Admin.php:192
    183156#: includes/Admin/MediaColumn.php:128
    184157msgid "Skipped"
    185158msgstr ""
    186159
    187 #: includes/Admin/Admin.php:202
     160#: includes/Admin/Admin.php:193
    188161#: includes/Admin/MediaColumn.php:141
    189162msgid "Optimizing…"
    190163msgstr ""
    191164
    192 #: includes/Admin/Admin.php:203
     165#: includes/Admin/Admin.php:194
    193166msgid "Restoring…"
    194167msgstr ""
    195168
     169#: includes/Admin/Admin.php:195
     170msgid "The server stopped responding. The bulk run has been paused. Press Resume to continue."
     171msgstr ""
     172
     173#: includes/Admin/Admin.php:196
     174msgid "Restore the original image? The optimized version and its WebP/AVIF copies will be removed."
     175msgstr ""
     176
     177#: includes/Admin/Admin.php:197
     178msgid "Restore ALL original images? Every optimized image goes back to its original file and all WebP/AVIF copies are removed. This can take a while."
     179msgstr ""
     180
     181#: includes/Admin/Admin.php:198
     182msgid "Settings saved"
     183msgstr ""
     184
     185#: includes/Admin/Admin.php:199
     186msgid "You have unsaved changes"
     187msgstr ""
     188
     189#: includes/Admin/Admin.php:200
     190msgid "Could not save settings"
     191msgstr ""
     192
     193#: includes/Admin/Admin.php:202
     194msgid "BytCrush is still optimizing. Leaving will pause the run."
     195msgstr ""
     196
     197#. translators: %s: number of images.
     198#: includes/Admin/Admin.php:205
     199#: views/dashboard.php:34
     200#, php-format
     201msgid "Optimize %s images in one click"
     202msgstr ""
     203
     204#. translators: %s: number of images.
     205#: includes/Admin/Admin.php:207
     206#: views/dashboard.php:32
     207#, php-format
     208msgid "%s images could not be optimized"
     209msgstr ""
     210
     211#. translators: %s: number of images.
     212#: includes/Admin/Admin.php:209
     213#, php-format
     214msgid "%s images left. Resume whenever you are ready."
     215msgstr ""
     216
     217#. translators: 1: processed count, 2: total count.
     218#: includes/Admin/Admin.php:211
     219#, php-format
     220msgid "%1$s of %2$s images"
     221msgstr ""
     222
     223#. translators: %s: time, e.g. 2m 10s.
     224#: includes/Admin/Admin.php:213
     225#, php-format
     226msgid "about %s left"
     227msgstr ""
     228
    196229#. translators: 1: bytes saved, 2: image count.
    197 #: includes/Admin/Admin.php:205
     230#: includes/Admin/Admin.php:215
    198231#, php-format
    199232msgid "Done! You saved %1$s across %2$s images."
    200233msgstr ""
    201234
    202 #: includes/Admin/Admin.php:206
    203 msgid "The server stopped responding. The bulk run has been paused. Press Resume to continue."
    204 msgstr ""
    205 
    206 #: includes/Admin/Admin.php:207
    207 msgid "Restore the original image? The optimized version and its WebP/AVIF copies will be removed."
    208 msgstr ""
    209 
    210 #: includes/Admin/Admin.php:208
    211 msgid "Restore ALL original images? Every optimized image goes back to its original file and all WebP/AVIF copies are removed. This can take a while."
    212 msgstr ""
    213 
    214 #. translators: %s: number of images.
    215 #: includes/Admin/Admin.php:210
     235#. translators: %s: number of images.
     236#: includes/Admin/Admin.php:217
    216237#, php-format
    217238msgid "Restored %s images to their originals."
    … …  
    219240
    220241#. translators: %s: number of images.
    221 #: includes/Admin/Admin.php:212
     242#: includes/Admin/Admin.php:219
    222243#, php-format
    223244msgid "%s failed images were put back in the queue."
    224245msgstr ""
    225246
    226 #: includes/Admin/Admin.php:213
    227 msgid "Settings saved"
    228 msgstr ""
    229 
    230 #: includes/Admin/Admin.php:214
    231 msgid "You have unsaved changes"
    232 msgstr ""
    233 
    234 #: includes/Admin/Admin.php:215
    235 msgid "Could not save settings"
    236 msgstr ""
    237 
    238 #: includes/Admin/Admin.php:216
    239 msgid "BytCrush is still optimizing. Leaving will pause the run."
    240 msgstr ""
    241 
    242 #: includes/Admin/Admin.php:261
     247#: includes/Admin/Admin.php:270
    243248msgid "BytCrush is ready."
    244249msgstr ""
    245250
    246251#. translators: %s: number of images.
    247 #: includes/Admin/Admin.php:263
     252#: includes/Admin/Admin.php:272
    248253#, php-format
    249254msgid "%s image in your Media Library can be optimized in one click. Originals are kept safe."
    … …  
    252257msgstr[1] ""
    253258
    254 #: includes/Admin/Admin.php:265
     259#: includes/Admin/Admin.php:274
    255260msgid "Optimize my images"
    256261msgstr ""
    257262
    258 #: includes/Admin/Admin.php:267
     263#: includes/Admin/Admin.php:276
    259264msgid "Dismiss"
    260265msgstr ""
    261266
    262 #: includes/Admin/Admin.php:291
     267#: includes/Admin/Admin.php:300
    263268msgid "Optimize"
    264269msgstr ""
    265270
    266 #: includes/Admin/Admin.php:334
     271#: includes/Admin/Admin.php:343
    267272msgid "picture tags"
    268273msgstr ""
    269274
    270 #: includes/Admin/Admin.php:335
     275#: includes/Admin/Admin.php:344
    271276msgid "server rewrite"
    272277msgstr ""
    273278
    274 #: includes/Admin/Admin.php:336
     279#: includes/Admin/Admin.php:345
    275280msgid "not served"
    276281msgstr ""
    277282
    278 #: includes/Admin/Admin.php:341
     283#: includes/Admin/Admin.php:350
    279284msgid "Compression"
    280285msgstr ""
    281286
    282 #: includes/Admin/Admin.php:343
     287#: includes/Admin/Admin.php:352
    283288#: views/settings.php:99
    284289msgid "Next-gen formats"
    … …  
    286291
    287292#. translators: 1: width, 2: height.
    288 #: includes/Admin/Admin.php:344
    289 #: includes/Admin/Admin.php:350
    290293#: includes/Admin/Admin.php:353
    291 #: includes/Admin/Admin.php:354
    292 #: includes/Admin/Admin.php:355
    293 #: includes/Admin/Admin.php:356
     294#: includes/Admin/Admin.php:359
     295#: includes/Admin/Admin.php:362
     296#: includes/Admin/Admin.php:363
     297#: includes/Admin/Admin.php:364
     298#: includes/Admin/Admin.php:365
    294299#: views/settings.php:121
    295300msgid "Off"
    296301msgstr ""
    297302
    298 #: includes/Admin/Admin.php:348
     303#: includes/Admin/Admin.php:357
    299304msgid "Resize large images"
    300305msgstr ""
    301306
    302307#. translators: 1: width, 2: height.
    303 #: includes/Admin/Admin.php:350
     308#: includes/Admin/Admin.php:359
    304309#, php-format
    305310msgid "Max %1$s × %2$s px"
    306311msgstr ""
    307312
    308 #: includes/Admin/Admin.php:353
     313#: includes/Admin/Admin.php:362
    309314#: views/settings.php:156
    310315msgid "Strip metadata"
    311316msgstr ""
    312317
    313 #: includes/Admin/Admin.php:353
     318#: includes/Admin/Admin.php:362
    314319msgid "On, colour profile kept"
    315320msgstr ""
    316321
    317 #: includes/Admin/Admin.php:354
     322#: includes/Admin/Admin.php:363
    318323msgid "Auto-optimize uploads"
    319324msgstr ""
    320325
    321 #: includes/Admin/Admin.php:354
    322 #: includes/Admin/Admin.php:355
    323 #: includes/Admin/Admin.php:356
     326#: includes/Admin/Admin.php:363
     327#: includes/Admin/Admin.php:364
     328#: includes/Admin/Admin.php:365
    324329msgid "On"
    325330msgstr ""
    326331
    327 #: includes/Admin/Admin.php:355
     332#: includes/Admin/Admin.php:364
    328333msgid "Keep originals"
    329334msgstr ""
    330335
    331 #: includes/Admin/Admin.php:356
     336#: includes/Admin/Admin.php:365
    332337msgid "Background runs"
    333338msgstr ""
    … …  
    390395msgstr ""
    391396
    392 #: includes/Engine/Background.php:400
     397#: includes/Engine/Background.php:425
    393398msgid "Every minute (BytCrush)"
    394399msgstr ""
    … …  
    445450msgstr ""
    446451
    447 #: includes/Engine/Compressor.php:222
     452#. translators: 1: image size in megapixels, 2: memory needed, 3: memory available.
     453#: includes/Engine/Compressor.php:186
     454#, php-format
     455msgid "Skipped to keep your site safe: this %1$s MP image needs about %2$s of memory and the server allows %3$s."
     456msgstr ""
     457
     458#: includes/Engine/Compressor.php:251
    448459msgid "The encoder returned an empty image."
    449460msgstr ""
    450461
    451462#. translators: %s: file name.
    452 #: includes/Engine/Compressor.php:247
     463#: includes/Engine/Compressor.php:276
    453464#, php-format
    454465msgid "Could not write %s. Check the uploads folder permissions."
    455466msgstr ""
    456467
    457 #: includes/Engine/Compressor.php:254
     468#: includes/Engine/Compressor.php:283
    458469msgid "Animated images are left untouched."
    459470msgstr ""
  • bytcrush-image-compression/tags/1.0.1/readme.txt

    r3753311 r3757771  
    55Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8888== Changelog ==
    8989
     90= 1.0.1 =
     91* Server-friendly by design: BytCrush now checks how much memory an image needs before opening it, and skips images that are too large for the server (or hands them to ImageMagick) instead of risking an error.
     92* Batches adapt to your host's time limit and proxy timeouts, and never start an image that could not finish in time.
     93* Optimization is paced by server load: it pauses between images when the server is busy, backs off when it is overloaded, and uses at most half the CPU when the load cannot be measured, so visitors keep priority.
     94* ImageMagick is limited to one thread and a memory budget that matches PHP's.
     95
    9096= 1.0.0 =
    9197* Initial release.
  • bytcrush-image-compression/trunk/assets/js/admin.js

    r3753311 r3757771  
    321321                                                return;
    322322                                        }
     323                                        if ( res.cooldown > 0 ) {
     324                                                // The server is overloaded: give it a break, then carry on.
     325                                                sub.textContent = i18n.cooling;
     326                                                for ( var waited = 0; waited < res.cooldown && run.active; waited++ ) {
     327                                                        await sleep( 1000 );
     328                                                }
     329                                                if ( run.active ) {
     330                                                        sub.textContent = i18n.subRunning;
     331                                                }
     332                                        }
    323333                                } catch ( err ) {
    324334                                        failures++;
  • bytcrush-image-compression/trunk/assets/js/background.js

    r3753311 r3757771  
    8989                                }
    9090
    91                                 if ( res.stalled ) {
     91                                if ( res.cooldown > 0 ) {
     92                                        // Server overloaded: the run is backing off and resumes by itself.
     93                                        d.setSub( t.subCooling );
     94                                        await sleep( Math.min( res.cooldown, 10 ) * 1000 );
     95                                } else if ( res.stalled ) {
    9296                                        // Loopback blocked or chain broken: this tab does the work.
    9397                                        d.setSub( t.subFallback );
  • bytcrush-image-compression/trunk/bytcrush-image-compression.php

    r3753311 r3757771  
    55 * Plugin URI:        https://warcal.dev/wordpress/plugins/bytcrush-image-compression
    66 * Description:       Optimize every image in your Media Library in one click with the BytCrush compression engine. Originals are always kept safe, and WebP/AVIF versions are served automatically.
    7  * Version:           1.0.0
     7 * Version:           1.0.1
    88 * Requires at least: 5.3
    99 * Requires PHP:      7.2
    … …  
    4141 * ---------------------------------------------------------------------------
    4242 */
    43 define( 'BYTC_VERSION', '1.0.0' );
     43define( 'BYTC_VERSION', '1.0.1' );
    4444define( 'BYTC_FILE', __FILE__ );
    4545define( 'BYTC_DIR', plugin_dir_path( __FILE__ ) );
  • bytcrush-image-compression/trunk/includes/Admin/Admin.php

    r3753311 r3757771  
    157157                'subBackground' => __( 'Running on your server. You can close this tab and come back any time.', 'bytcrush-image-compression' ),
    158158                'subFallback'   => __( 'Your server is blocking background requests, so this tab is doing the work. Keep it open to continue.', 'bytcrush-image-compression' ),
     159                'subCooling'    => __( 'Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically.', 'bytcrush-image-compression' ),
    159160            ),
    160161        ) );
    … …  
    230231            'unsaved'           => __( 'You have unsaved changes', 'bytcrush-image-compression' ),
    231232            'saveFailed'        => __( 'Could not save settings', 'bytcrush-image-compression' ),
     233            'cooling'           => __( 'Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically.', 'bytcrush-image-compression' ),
    232234            'leaveWarning'      => __( 'BytCrush is still optimizing. Leaving will pause the run.', 'bytcrush-image-compression' ),
    233235        );
  • bytcrush-image-compression/trunk/includes/Engine/Background.php

    r3753311 r3757771  
    101101                                'halted'    => '',
    102102                                'finished'  => 0,
     103                                'resume_at' => 0,
    103104                        )
    104105                );
    … …  
    128129                                        'halted'    => '',
    129130                                        'finished'  => 0,
     131                                        'resume_at' => 0,
    130132                                )
    131133                        );
    … …  
    216218                        return;
    217219                }
    218                 if ( self::stalled() ) {
     220                $state = self::state();
     221                if ( self::stalled() || ( $state['resume_at'] && ! self::cooling() && ! Lock::since( self::LOCK ) ) ) {
    219222                        self::work( true );
    220223                }
    … …  
    227230         */
    228231        public static function stalled() {
     232                if ( self::cooling() ) {
     233                        return false; // Deliberate back-off, not a broken chain.
     234                }
    229235                $locked = Lock::since( self::LOCK );
    230236                if ( $locked && ( time() - $locked ) < 5 * MINUTE_IN_SECONDS ) {
    … …  
    241247         */
    242248        public static function work( $chain ) {
    243                 if ( ! self::running() || ! Lock::acquire( self::LOCK, 5 * MINUTE_IN_SECONDS ) ) {
     249                // Cooling down after an overloaded batch: the cron job resumes the run.
     250                if ( ! self::running() || self::cooling() || ! Lock::acquire( self::LOCK, 5 * MINUTE_IN_SECONDS ) ) {
    244251                        return array();
    245252                }
    246253
    247                 $items  = array();
    248                 $halted = '';
     254                $items    = array();
     255                $halted   = '';
     256                $cooldown = 0;
    249257
    250258                try {
    251259                        self::beat();
    252                         $budget = (float) apply_filters( 'bytcrush_bulk_time_budget', 20 );
    253                         $start  = microtime( true );
    254260                        $seen   = array();
    255261                        $streak = 0;
    256262
    257                         do {
    258                                 if ( ! self::running() ) {
    259                                         break;
    260                                 }
    261                                 $ids = Stats::pending_ids( 1, $seen );
    262                                 if ( ! $ids ) {
    263                                         break;
    264                                 }
    265                                 $seen[]  = $ids[0];
    266                                 $result  = Optimizer::optimize( $ids[0] );
    267                                 $items[] = self::record( Controller::item( $ids[0], $result ) );
    268 
    269                                 $streak = 'error' === $result['status'] ? $streak + 1 : 0;
    270                                 if ( $streak >= 10 ) {
    271                                         $halted = $result['message'];
    272                                         break;
    273                                 }
    274                         } while ( microtime( true ) - $start < $budget );
     263                        // Time-boxed to the host's limits and paced by server load.
     264                        $batch    = Resources::batch(
     265                                static function () use ( &$seen ) {
     266                                        $ids = Stats::pending_ids( 1, $seen );
     267                                        return $ids ? $ids[0] : null;
     268                                },
     269                                static function ( $id ) use ( &$seen, &$items, &$streak, &$halted ) {
     270                                        $seen[]  = $id;
     271                                        $result  = Optimizer::optimize( $id );
     272                                        $items[] = self::record( Controller::item( $id, $result ) );
     273
     274                                        $streak = 'error' === $result['status'] ? $streak + 1 : 0;
     275                                        if ( $streak >= 10 ) {
     276                                                $halted = $result['message'];
     277                                                return true;
     278                                        }
     279                                        return false;
     280                                },
     281                                array( __CLASS__, 'running' )
     282                        );
     283                        $cooldown = $batch['cooldown'];
    275284                } finally {
    276285                        Lock::release( self::LOCK );
    … …  
    281290                } elseif ( ! Stats::pending_ids( 1 ) ) {
    282291                        self::stop();
     292                } elseif ( $cooldown ) {
     293                        // Server overloaded: back off; cron (or the open tab) resumes after.
     294                        $state              = self::state();
     295                        $state['resume_at'] = time() + $cooldown;
     296                        $state['beat']      = time();
     297                        self::save_state( $state );
    283298                } elseif ( $chain && self::running() ) {
    284299                        self::dispatch();
    … …  
    286301
    287302                return $items;
     303        }
     304
     305        /**
     306         * Whether the run is backing off because the server was overloaded.
     307         *
     308         * @return bool
     309         */
     310        public static function cooling() {
     311                return (int) self::state()['resume_at'] > time();
    288312        }
    289313
    … …  
    301325                        'running'   => $running,
    302326                        'stalled'   => $running && self::stalled(),
     327                        'cooldown'  => $running ? max( 0, (int) $state['resume_at'] - time() ) : 0,
    303328                        'processed' => (int) $state['processed'],
    304329                        'elapsed'   => $state['started'] ? max( 1, time() - (int) $state['started'] ) : 0,
  • bytcrush-image-compression/trunk/includes/Engine/Compressor.php

    r3753311 r3757771  
    164164                                sprintf( __( 'This server cannot create %s images.', 'bytcrush-image-compression' ), strtoupper( $format ) )
    165165                        );
     166                }
     167
     168                // Check memory before decoding: GD holds every pixel in PHP memory, and a
     169                // huge image would kill the request. Imagick works outside PHP's limit
     170                // (with its own capped budget that spills to disk), so hand it over;
     171                // with no alternative, skip the image instead of crashing.
     172                if ( GdDriver::class === $class ) {
     173                        $size = @getimagesize( $src ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- header read only.
     174                        if ( $size ) {
     175                                list( $target_w, $target_h ) = self::fitted_size( (int) $size[0], (int) $size[1], (int) $o['max_width'], (int) $o['max_height'] );
     176                                $needed                      = Resources::gd_memory_needed( (int) $size[0], (int) $size[1], $target_w, $target_h );
     177                                $available                   = Resources::memory_available();
     178                                if ( $needed > $available ) {
     179                                        if ( ImagickDriver::available() && ImagickDriver::can_encode( $format ) ) {
     180                                                $class = ImagickDriver::class;
     181                                        } else {
     182                                                return new \WP_Error(
     183                                                        'bytcrush_too_large',
     184                                                        sprintf(
     185                                                                /* translators: 1: image size in megapixels, 2: memory needed, 3: memory available. */
     186                                                                __( 'Skipped to keep your site safe: this %1$s MP image needs about %2$s of memory and the server allows %3$s.', 'bytcrush-image-compression' ),
     187                                                                number_format_i18n( $size[0] * $size[1] / 1000000, 1 ),
     188                                                                size_format( $needed ),
     189                                                                size_format( $available )
     190                                                        )
     191                                                );
     192                                        }
     193                                }
     194                        }
    166195                }
    167196
    … …  
    270299         */
    271300        private static function fit( Driver $driver, $max_width, $max_height ) {
    272                 if ( $max_width <= 0 && $max_height <= 0 ) {
     301                list( $width, $height ) = self::fitted_size( $driver->width(), $driver->height(), $max_width, $max_height );
     302                if ( $width === $driver->width() && $height === $driver->height() ) {
    273303                        return false;
    274304                }
    275                 $w     = $driver->width();
    276                 $h     = $driver->height();
     305                $driver->resize( $width, $height );
     306                return true;
     307        }
     308
     309        /**
     310         * Dimensions after fitting inside a box (unchanged when already inside).
     311         *
     312         * @param int $w          Width.
     313         * @param int $h          Height.
     314         * @param int $max_width  Max width (0 = unbounded).
     315         * @param int $max_height Max height (0 = unbounded).
     316         * @return int[] { width, height }
     317         */
     318        private static function fitted_size( $w, $h, $max_width, $max_height ) {
     319                if ( ( $max_width <= 0 && $max_height <= 0 ) || $w < 1 || $h < 1 ) {
     320                        return array( $w, $h );
     321                }
    277322                $box_w = $max_width > 0 ? $max_width : $w;
    278323                $box_h = $max_height > 0 ? $max_height : $h;
    279324                if ( $w <= $box_w && $h <= $box_h ) {
    280                         return false;
     325                        return array( $w, $h );
    281326                }
    282327                $ratio = min( $box_w / $w, $box_h / $h );
    283                 $driver->resize( max( 1, (int) round( $w * $ratio ) ), max( 1, (int) round( $h * $ratio ) ) );
    284                 return true;
     328                return array( max( 1, (int) round( $w * $ratio ) ), max( 1, (int) round( $h * $ratio ) ) );
    285329        }
    286330
  • bytcrush-image-compression/trunk/includes/Engine/Driver/ImagickDriver.php

    r3753311 r3757771  
    5454                }
    5555
     56                self::limit_resources();
    5657                $im = new \Imagick();
    5758                $im->readImage( $path );
    … …  
    6364
    6465                $this->im = $im;
     66        }
     67
     68        /**
     69         * Keep ImageMagick within what the server can spare. Its memory is not
     70         * counted against PHP's memory_limit and it uses every CPU core by default,
     71         * which on shared hosting can get the process throttled or killed. Cap it
     72         * to one thread (as WordPress core does) and to a memory budget that
     73         * matches PHP's; above that, its pixel cache spills to disk instead.
     74         *
     75         * @return void
     76         */
     77        private static function limit_resources() {
     78                static $done = false;
     79                if ( $done || ! method_exists( '\Imagick', 'setResourceLimit' ) ) {
     80                        return;
     81                }
     82                $done = true;
     83
     84                /**
     85                 * Threads ImageMagick may use per image (0 = leave its default).
     86                 *
     87                 * @param int $threads Threads.
     88                 */
     89                $threads = (int) apply_filters( 'bytcrush_imagick_threads', 1 );
     90                if ( $threads > 0 && defined( '\Imagick::RESOURCETYPE_THREAD' ) ) {
     91                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_THREAD, $threads );
     92                }
     93
     94                $memory = (int) min( 256 * MB_IN_BYTES, max( 32 * MB_IN_BYTES, \BytCrush\Engine\Resources::memory_available() ) );
     95                if ( defined( '\Imagick::RESOURCETYPE_MEMORY' ) ) {
     96                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_MEMORY, $memory );
     97                }
     98                if ( defined( '\Imagick::RESOURCETYPE_MAP' ) ) {
     99                        \Imagick::setResourceLimit( \Imagick::RESOURCETYPE_MAP, $memory * 2 );
     100                }
    65101        }
    66102
  • bytcrush-image-compression/trunk/includes/Engine/Optimizer.php

    r3753311 r3757771  
    277277
    278278                                if ( is_wp_error( $res ) ) {
    279                                         // Animated GIF/PNG: skip the image (as the WebP step does), don't flag an error.
    280                                         if ( 'bytcrush_animated' === $res->get_error_code() ) {
     279                                        // Animated, or too large for this server's memory: skip the image with the reason, don't flag an error.
     280                                        if ( in_array( $res->get_error_code(), array( 'bytcrush_animated', 'bytcrush_too_large' ), true ) ) {
    281281                                                return self::finish( $id, 'skipped', $res->get_error_message(), $meta );
    282282                                        }
    … …  
    341341
    342342                                if ( is_wp_error( $bytes ) ) {
    343                                         if ( 'bytcrush_animated' === $bytes->get_error_code() ) {
     343                                        if ( in_array( $bytes->get_error_code(), array( 'bytcrush_animated', 'bytcrush_too_large' ), true ) ) {
    344344                                                return self::finish( $id, 'skipped', $bytes->get_error_message(), $meta );
    345345                                        }
  • bytcrush-image-compression/trunk/includes/Rest/Controller.php

    r3753311 r3757771  
    1818use BytCrush\Engine\Backup;
    1919use BytCrush\Engine\Optimizer;
     20use BytCrush\Engine\Resources;
    2021use BytCrush\Engine\Stats;
    2122use WP_REST_Request;
    … …  
    164165                bytcrush_keep_running();
    165166
    166                 /**
    167                  * Seconds of work per bulk request. Keep well under the host's
    168                  * max_execution_time / proxy timeout.
    169                  *
    170                  * @param float $seconds Budget.
    171                  */
    172                 $budget = (float) apply_filters( 'bytcrush_bulk_time_budget', 20 );
    173                 $start  = microtime( true );
    174167                $items  = array();
    175168                $seen   = array();
    … …  
    177170                $halted = '';
    178171
    179                 do {
    180                         $ids = Stats::pending_ids( 1, $seen );
    181                         if ( ! $ids ) {
    182                                 break;
     172                // Time-boxed to the host's limits and paced by server load.
     173                $batch = Resources::batch(
     174                        static function () use ( &$seen ) {
     175                                $ids = Stats::pending_ids( 1, $seen );
     176                                return $ids ? $ids[0] : null;
     177                        },
     178                        static function ( $id ) use ( &$seen, &$items, &$streak, &$halted ) {
     179                                $seen[]  = $id;
     180                                $result  = Optimizer::optimize( $id );
     181                                $items[] = self::item( $id, $result );
     182
     183                                // A run of consecutive failures means a site-wide problem (disk
     184                                // full, permissions): stop instead of failing the whole library.
     185                                $streak = 'error' === $result['status'] ? $streak + 1 : 0;
     186                                if ( $streak >= 10 ) {
     187                                        $halted = $result['message'];
     188                                        return true;
     189                                }
     190                                return false;
    183191                        }
    184                         $seen[]  = $ids[0];
    185                         $result  = Optimizer::optimize( $ids[0] );
    186                         $items[] = self::item( $ids[0], $result );
    187 
    188                         // A run of consecutive failures means a site-wide problem (disk
    189                         // full, permissions): stop instead of failing the whole library.
    190                         $streak = 'error' === $result['status'] ? $streak + 1 : 0;
    191                         if ( $streak >= 10 ) {
    192                                 $halted = $result['message'];
    193                                 break;
    194                         }
    195                 } while ( microtime( true ) - $start < $budget );
    196 
    197                 return rest_ensure_response(
    198                         array(
    199                                 'items'  => $items,
    200                                 'stats'  => Stats::get(),
    201                                 'done'   => ! Stats::pending_ids( 1 ),
    202                                 'halted' => $halted,
     192                );
     193
     194                return rest_ensure_response(
     195                        array(
     196                                'items'    => $items,
     197                                'stats'    => Stats::get(),
     198                                'done'     => ! Stats::pending_ids( 1 ),
     199                                'halted'   => $halted,
     200                                'cooldown' => $batch['cooldown'],
    203201                        )
    204202                );
  • bytcrush-image-compression/trunk/languages/bytcrush-image-compression.pot

    r3753311 r3757771  
    33msgid ""
    44msgstr ""
    5 "Project-Id-Version: BytCrush Image Compression 1.0.0\n"
     5"Project-Id-Version: BytCrush Image Compression 1.0.1\n"
    66"Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/app\n"
    77"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
    … …  
    1010"Content-Type: text/plain; charset=UTF-8\n"
    1111"Content-Transfer-Encoding: 8bit\n"
    12 "POT-Creation-Date: 2026-10-14T15:53:40+00:00\n"
     12"POT-Creation-Date: 2026-10-21T04:14:25+00:00\n"
    1313"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    1414"X-Generator: WP-CLI 2.12.0\n"
    … …  
    1717#. Plugin Name of the plugin
    1818#: bytcrush-image-compression.php
    19 #: includes/Admin/Admin.php:61
     19#: includes/Admin/Admin.php:62
    2020#: views/dashboard.php:46
    2121msgid "BytCrush Image Compression"
    … …  
    4848msgstr ""
    4949
    50 #: includes/Admin/Admin.php:62
     50#: includes/Admin/Admin.php:63
    5151#: includes/Admin/MediaColumn.php:38
    5252#: includes/Admin/MediaColumn.php:65
    … …  
    5454msgstr ""
    5555
    56 #: includes/Admin/Admin.php:68
     56#: includes/Admin/Admin.php:69
    5757#: views/header.php:15
    5858msgid "Dashboard"
    5959msgstr ""
    6060
    61 #: includes/Admin/Admin.php:71
     61#: includes/Admin/Admin.php:72
    6262#: views/header.php:20
    6363msgid "Page audit"
    6464msgstr ""
    6565
    66 #: includes/Admin/Admin.php:73
    67 #: includes/Admin/Admin.php:292
     66#: includes/Admin/Admin.php:74
     67#: includes/Admin/Admin.php:301
    6868#: views/header.php:16
    6969msgid "Settings"
    7070msgstr ""
    7171
    72 #: includes/Admin/Admin.php:76
     72#: includes/Admin/Admin.php:77
    7373#: views/pro.php:33
    7474msgid "BytCrush Pro"
    7575msgstr ""
    7676
    77 #: includes/Admin/Admin.php:76
    78 #: includes/Admin/Admin.php:295
     77#: includes/Admin/Admin.php:77
     78#: includes/Admin/Admin.php:304
    7979#: views/header.php:23
    8080msgid "Go Pro"
    8181msgstr ""
    8282
    83 #: includes/Admin/Admin.php:133
     83#: includes/Admin/Admin.php:135
    8484msgid "Running on your server. You can close this tab and come back any time."
    8585msgstr ""
    8686
    87 #: includes/Admin/Admin.php:134
     87#: includes/Admin/Admin.php:136
    8888msgid "Your server is blocking background requests, so this tab is doing the work. Keep it open to continue."
    8989msgstr ""
    9090
    91 #. translators: %s: number of images.
     91#: includes/Admin/Admin.php:137
     92#: includes/Admin/Admin.php:201
     93msgid "Your server is busy right now, so BytCrush is taking a short break to keep your site fast. It continues automatically."
     94msgstr ""
     95
    9296#: includes/Admin/Admin.php:179
    93 #: views/dashboard.php:34
    94 #, php-format
    95 msgid "Optimize %s images in one click"
     97msgid "Crushing bytes…"
    9698msgstr ""
    9799
    98100#: includes/Admin/Admin.php:180
    99 msgid "Crushing bytes…"
     101msgid "Paused"
    100102msgstr ""
    101103
    102104#: includes/Admin/Admin.php:181
    103 msgid "Paused"
    104 msgstr ""
    105 
    106 #: includes/Admin/Admin.php:182
    107105#: views/dashboard.php:35
    108106msgid "Your library is fully optimized"
    109107msgstr ""
    110108
    111 #. translators: %s: number of images.
    112 #: includes/Admin/Admin.php:184
    113 #: views/dashboard.php:32
    114 #, php-format
    115 msgid "%s images could not be optimized"
    116 msgstr ""
    117 
    118 #: includes/Admin/Admin.php:185
     109#: includes/Admin/Admin.php:182
    119110#: views/dashboard.php:39
    120111msgid "The reasons are listed below. Fix the cause, then press Retry."
    121112msgstr ""
    122113
     114#: includes/Admin/Admin.php:183
     115msgid "Paused after 10 failures in a row. "
     116msgstr ""
     117
     118#: includes/Admin/Admin.php:184
     119msgid "Restoring originals…"
     120msgstr ""
     121
     122#: includes/Admin/Admin.php:185
     123msgid "Keep this tab open. You can pause at any time and pick up where you left off."
     124msgstr ""
     125
    123126#: includes/Admin/Admin.php:186
    124 msgid "Paused after 10 failures in a row. "
    125 msgstr ""
    126 
    127 #: includes/Admin/Admin.php:187
    128 msgid "Restoring originals…"
    129 msgstr ""
    130 
    131 #: includes/Admin/Admin.php:188
    132 msgid "Keep this tab open. You can pause at any time and pick up where you left off."
    133 msgstr ""
    134 
    135 #. translators: %s: number of images.
    136 #: includes/Admin/Admin.php:190
    137 #, php-format
    138 msgid "%s images left. Resume whenever you are ready."
    139 msgstr ""
    140 
    141 #: includes/Admin/Admin.php:191
    142127#: views/dashboard.php:41
    143128msgid "New uploads are optimized automatically. Originals are safe in backups."
    144129msgstr ""
    145130
    146 #: includes/Admin/Admin.php:192
     131#: includes/Admin/Admin.php:187
    147132#: views/dashboard.php:93
    148133msgid "Optimize all images"
    149134msgstr ""
    150135
    151 #: includes/Admin/Admin.php:193
     136#: includes/Admin/Admin.php:188
    152137msgid "Pause"
    153138msgstr ""
    154139
    155 #: includes/Admin/Admin.php:194
     140#: includes/Admin/Admin.php:189
    156141msgid "Resume"
    157142msgstr ""
    158143
    159 #: includes/Admin/Admin.php:195
     144#: includes/Admin/Admin.php:190
    160145#: views/dashboard.php:93
    161146msgid "All images optimized"
    162147msgstr ""
    163148
    164 #. translators: 1: processed count, 2: total count.
    165 #: includes/Admin/Admin.php:197
    166 #, php-format
    167 msgid "%1$s of %2$s images"
    168 msgstr ""
    169 
    170 #. translators: %s: time, e.g. 2m 10s.
    171 #: includes/Admin/Admin.php:199
    172 #, php-format
    173 msgid "about %s left"
    174 msgstr ""
    175 
    176 #: includes/Admin/Admin.php:200
     149#: includes/Admin/Admin.php:191
    177150#: includes/Admin/MediaColumn.php:128
    178151#: views/dashboard.php:220
    … …  
    180153msgstr ""
    181154
    182 #: includes/Admin/Admin.php:201
     155#: includes/Admin/Admin.php:192
    183156#: includes/Admin/MediaColumn.php:128
    184157msgid "Skipped"
    185158msgstr ""
    186159
    187 #: includes/Admin/Admin.php:202
     160#: includes/Admin/Admin.php:193
    188161#: includes/Admin/MediaColumn.php:141
    189162msgid "Optimizing…"
    190163msgstr ""
    191164
    192 #: includes/Admin/Admin.php:203
     165#: includes/Admin/Admin.php:194
    193166msgid "Restoring…"
    194167msgstr ""
    195168
     169#: includes/Admin/Admin.php:195
     170msgid "The server stopped responding. The bulk run has been paused. Press Resume to continue."
     171msgstr ""
     172
     173#: includes/Admin/Admin.php:196
     174msgid "Restore the original image? The optimized version and its WebP/AVIF copies will be removed."
     175msgstr ""
     176
     177#: includes/Admin/Admin.php:197
     178msgid "Restore ALL original images? Every optimized image goes back to its original file and all WebP/AVIF copies are removed. This can take a while."
     179msgstr ""
     180
     181#: includes/Admin/Admin.php:198
     182msgid "Settings saved"
     183msgstr ""
     184
     185#: includes/Admin/Admin.php:199
     186msgid "You have unsaved changes"
     187msgstr ""
     188
     189#: includes/Admin/Admin.php:200
     190msgid "Could not save settings"
     191msgstr ""
     192
     193#: includes/Admin/Admin.php:202
     194msgid "BytCrush is still optimizing. Leaving will pause the run."
     195msgstr ""
     196
     197#. translators: %s: number of images.
     198#: includes/Admin/Admin.php:205
     199#: views/dashboard.php:34
     200#, php-format
     201msgid "Optimize %s images in one click"
     202msgstr ""
     203
     204#. translators: %s: number of images.
     205#: includes/Admin/Admin.php:207
     206#: views/dashboard.php:32
     207#, php-format
     208msgid "%s images could not be optimized"
     209msgstr ""
     210
     211#. translators: %s: number of images.
     212#: includes/Admin/Admin.php:209
     213#, php-format
     214msgid "%s images left. Resume whenever you are ready."
     215msgstr ""
     216
     217#. translators: 1: processed count, 2: total count.
     218#: includes/Admin/Admin.php:211
     219#, php-format
     220msgid "%1$s of %2$s images"
     221msgstr ""
     222
     223#. translators: %s: time, e.g. 2m 10s.
     224#: includes/Admin/Admin.php:213
     225#, php-format
     226msgid "about %s left"
     227msgstr ""
     228
    196229#. translators: 1: bytes saved, 2: image count.
    197 #: includes/Admin/Admin.php:205
     230#: includes/Admin/Admin.php:215
    198231#, php-format
    199232msgid "Done! You saved %1$s across %2$s images."
    200233msgstr ""
    201234
    202 #: includes/Admin/Admin.php:206
    203 msgid "The server stopped responding. The bulk run has been paused. Press Resume to continue."
    204 msgstr ""
    205 
    206 #: includes/Admin/Admin.php:207
    207 msgid "Restore the original image? The optimized version and its WebP/AVIF copies will be removed."
    208 msgstr ""
    209 
    210 #: includes/Admin/Admin.php:208
    211 msgid "Restore ALL original images? Every optimized image goes back to its original file and all WebP/AVIF copies are removed. This can take a while."
    212 msgstr ""
    213 
    214 #. translators: %s: number of images.
    215 #: includes/Admin/Admin.php:210
     235#. translators: %s: number of images.
     236#: includes/Admin/Admin.php:217
    216237#, php-format
    217238msgid "Restored %s images to their originals."
    … …  
    219240
    220241#. translators: %s: number of images.
    221 #: includes/Admin/Admin.php:212
     242#: includes/Admin/Admin.php:219
    222243#, php-format
    223244msgid "%s failed images were put back in the queue."
    224245msgstr ""
    225246
    226 #: includes/Admin/Admin.php:213
    227 msgid "Settings saved"
    228 msgstr ""
    229 
    230 #: includes/Admin/Admin.php:214
    231 msgid "You have unsaved changes"
    232 msgstr ""
    233 
    234 #: includes/Admin/Admin.php:215
    235 msgid "Could not save settings"
    236 msgstr ""
    237 
    238 #: includes/Admin/Admin.php:216
    239 msgid "BytCrush is still optimizing. Leaving will pause the run."
    240 msgstr ""
    241 
    242 #: includes/Admin/Admin.php:261
     247#: includes/Admin/Admin.php:270
    243248msgid "BytCrush is ready."
    244249msgstr ""
    245250
    246251#. translators: %s: number of images.
    247 #: includes/Admin/Admin.php:263
     252#: includes/Admin/Admin.php:272
    248253#, php-format
    249254msgid "%s image in your Media Library can be optimized in one click. Originals are kept safe."
    … …  
    252257msgstr[1] ""
    253258
    254 #: includes/Admin/Admin.php:265
     259#: includes/Admin/Admin.php:274
    255260msgid "Optimize my images"
    256261msgstr ""
    257262
    258 #: includes/Admin/Admin.php:267
     263#: includes/Admin/Admin.php:276
    259264msgid "Dismiss"
    260265msgstr ""
    261266
    262 #: includes/Admin/Admin.php:291
     267#: includes/Admin/Admin.php:300
    263268msgid "Optimize"
    264269msgstr ""
    265270
    266 #: includes/Admin/Admin.php:334
     271#: includes/Admin/Admin.php:343
    267272msgid "picture tags"
    268273msgstr ""
    269274
    270 #: includes/Admin/Admin.php:335
     275#: includes/Admin/Admin.php:344
    271276msgid "server rewrite"
    272277msgstr ""
    273278
    274 #: includes/Admin/Admin.php:336
     279#: includes/Admin/Admin.php:345
    275280msgid "not served"
    276281msgstr ""
    277282
    278 #: includes/Admin/Admin.php:341
     283#: includes/Admin/Admin.php:350
    279284msgid "Compression"
    280285msgstr ""
    281286
    282 #: includes/Admin/Admin.php:343
     287#: includes/Admin/Admin.php:352
    283288#: views/settings.php:99
    284289msgid "Next-gen formats"
    … …  
    286291
    287292#. translators: 1: width, 2: height.
    288 #: includes/Admin/Admin.php:344
    289 #: includes/Admin/Admin.php:350
    290293#: includes/Admin/Admin.php:353
    291 #: includes/Admin/Admin.php:354
    292 #: includes/Admin/Admin.php:355
    293 #: includes/Admin/Admin.php:356
     294#: includes/Admin/Admin.php:359
     295#: includes/Admin/Admin.php:362
     296#: includes/Admin/Admin.php:363
     297#: includes/Admin/Admin.php:364
     298#: includes/Admin/Admin.php:365
    294299#: views/settings.php:121
    295300msgid "Off"
    296301msgstr ""
    297302
    298 #: includes/Admin/Admin.php:348
     303#: includes/Admin/Admin.php:357
    299304msgid "Resize large images"
    300305msgstr ""
    301306
    302307#. translators: 1: width, 2: height.
    303 #: includes/Admin/Admin.php:350
     308#: includes/Admin/Admin.php:359
    304309#, php-format
    305310msgid "Max %1$s × %2$s px"
    306311msgstr ""
    307312
    308 #: includes/Admin/Admin.php:353
     313#: includes/Admin/Admin.php:362
    309314#: views/settings.php:156
    310315msgid "Strip metadata"
    311316msgstr ""
    312317
    313 #: includes/Admin/Admin.php:353
     318#: includes/Admin/Admin.php:362
    314319msgid "On, colour profile kept"
    315320msgstr ""
    316321
    317 #: includes/Admin/Admin.php:354
     322#: includes/Admin/Admin.php:363
    318323msgid "Auto-optimize uploads"
    319324msgstr ""
    320325
    321 #: includes/Admin/Admin.php:354
    322 #: includes/Admin/Admin.php:355
    323 #: includes/Admin/Admin.php:356
     326#: includes/Admin/Admin.php:363
     327#: includes/Admin/Admin.php:364
     328#: includes/Admin/Admin.php:365
    324329msgid "On"
    325330msgstr ""
    326331
    327 #: includes/Admin/Admin.php:355
     332#: includes/Admin/Admin.php:364
    328333msgid "Keep originals"
    329334msgstr ""
    330335
    331 #: includes/Admin/Admin.php:356
     336#: includes/Admin/Admin.php:365
    332337msgid "Background runs"
    333338msgstr ""
    … …  
    390395msgstr ""
    391396
    392 #: includes/Engine/Background.php:400
     397#: includes/Engine/Background.php:425
    393398msgid "Every minute (BytCrush)"
    394399msgstr ""
    … …  
    445450msgstr ""
    446451
    447 #: includes/Engine/Compressor.php:222
     452#. translators: 1: image size in megapixels, 2: memory needed, 3: memory available.
     453#: includes/Engine/Compressor.php:186
     454#, php-format
     455msgid "Skipped to keep your site safe: this %1$s MP image needs about %2$s of memory and the server allows %3$s."
     456msgstr ""
     457
     458#: includes/Engine/Compressor.php:251
    448459msgid "The encoder returned an empty image."
    449460msgstr ""
    450461
    451462#. translators: %s: file name.
    452 #: includes/Engine/Compressor.php:247
     463#: includes/Engine/Compressor.php:276
    453464#, php-format
    454465msgid "Could not write %s. Check the uploads folder permissions."
    455466msgstr ""
    456467
    457 #: includes/Engine/Compressor.php:254
     468#: includes/Engine/Compressor.php:283
    458469msgid "Animated images are left untouched."
    459470msgstr ""
  • bytcrush-image-compression/trunk/readme.txt

    r3753311 r3757771  
    55Tested up to: 7.1
    66Requires PHP: 7.2
    7 Stable tag: 1.0.0
     7Stable tag: 1.0.1
    88License: GPLv2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    … …  
    8888== Changelog ==
    8989
     90= 1.0.1 =
     91* Server-friendly by design: BytCrush now checks how much memory an image needs before opening it, and skips images that are too large for the server (or hands them to ImageMagick) instead of risking an error.
     92* Batches adapt to your host's time limit and proxy timeouts, and never start an image that could not finish in time.
     93* Optimization is paced by server load: it pauses between images when the server is busy, backs off when it is overloaded, and uses at most half the CPU when the load cannot be measured, so visitors keep priority.
     94* ImageMagick is limited to one thread and a memory budget that matches PHP's.
     95
    9096= 1.0.0 =
    9197* Initial release.
Note: See TracChangeset for help on using the changeset viewer.