Plugin Directory

Changeset 3791597


Ignore:
Timestamp:
11/08/2026 10:58:25 PM (less than one hour ago)
Author:
striffly
Message:

Update to version 1.0.1 from GitHub

Location:
takopic
Files:
30 edited
1 copied

Legend:

Unmodified
Added
Removed
  • takopic/tags/1.0.1/assets/js/components/Count.js

    r3791227 r3791597  
    99
    1010(() => {
    11         const {Component, Roll} = window.takopic;
     11        const {Component, Roll, Screen} = window.takopic;
    1212
    1313        class Count extends Component {
    … …  
    4141                 */
    4242                show(value) {
    43                         this.el.textContent = value.toLocaleString();
     43                        this.el.textContent = Screen.number(value);
    4444                }
    4545
  • takopic/tags/1.0.1/assets/js/components/DeactivationWarning.js

    r3791227 r3791597  
    1515(() => {
    1616        const {_n, sprintf} = wp.i18n;
    17         const {Component} = window.takopic;
     17        const {Component, Screen} = window.takopic;
    1818
    1919        class DeactivationWarning extends Component {
    … …  
    6565                        }
    6666                        /* translators: %s: the number of images. */
    67                         this.images.textContent = sprintf(_n('%s image', '%s images', images, 'takopic'), images.toLocaleString());
     67                        this.images.textContent = sprintf(_n('%s image', '%s images', images, 'takopic'), Screen.number(images));
    6868                        this.size.textContent = size ? ` (${size})` : '';
    6969                        this.originals.hidden = !images;
  • takopic/tags/1.0.1/assets/js/components/Job.js

    r3791227 r3791597  
    1818(() => {
    1919        const {__, _n, sprintf} = wp.i18n;
    20         const {Component, Confetti} = window.takopic;
     20        const {Component, Confetti, Screen} = window.takopic;
    2121
    2222        class Job extends Component {
    … …  
    6262                                cancelled: __('Optimisation cancelled', 'takopic'),
    6363                                /* translators: %s: the number of images. */
    64                                 processed: (count) => Job.emphasise(_n('%s image optimised', '%s images optimised', count, 'takopic'), count.toLocaleString()),
     64                                processed: (count) => Job.emphasise(_n('%s image optimised', '%s images optimised', count, 'takopic'), Screen.number(count)),
    6565                        },
    6666                        remove_formats: {
    … …  
    6969                                cancelled: __('Removal cancelled', 'takopic'),
    7070                                /* translators: %s: the number of images. */
    71                                 processed: (count) => Job.emphasise(_n('%s image cleaned up', '%s images cleaned up', count, 'takopic'), count.toLocaleString()),
     71                                processed: (count) => Job.emphasise(_n('%s image cleaned up', '%s images cleaned up', count, 'takopic'), Screen.number(count)),
    7272                        },
    7373                        restore: {
    … …  
    7676                                cancelled: __('Restore cancelled', 'takopic'),
    7777                                /* translators: %s: the number of images. */
    78                                 processed: (count) => Job.emphasise(_n('%s image restored', '%s images restored', count, 'takopic'), count.toLocaleString()),
     78                                processed: (count) => Job.emphasise(_n('%s image restored', '%s images restored', count, 'takopic'), Screen.number(count)),
    7979                        },
    8080                };
    … …  
    340340
    341341                        /* translators: 1: the number of images processed, 2: the number of images. */
    342                         let progress = sprintf(_n('%1$s / %2$s image', '%1$s / %2$s images', job.total, 'takopic'), job.done.toLocaleString(), job.total.toLocaleString());
     342                        let progress = sprintf(_n('%1$s / %2$s image', '%1$s / %2$s images', job.total, 'takopic'), Screen.number(job.done), Screen.number(job.total));
    343343                        if (job.failed) {
    344344                                /* translators: 1: the progress, e.g. 3 / 12 images, 2: the number of images with errors. */
    345                                 progress = sprintf(_n('%1$s, %2$s with errors', '%1$s, %2$s with errors', job.failed, 'takopic'), progress, job.failed.toLocaleString());
     345                                progress = sprintf(_n('%1$s, %2$s with errors', '%1$s, %2$s with errors', job.failed, 'takopic'), progress, Screen.number(job.failed));
    346346                        }
    347347                        if (job.waiting) {
    348348                                // Held by another request, e.g. a worker PHP stopped: its lock goes stale within a minute.
    349349                                /* translators: 1: the progress, e.g. 3 / 12 images, 2: a number of seconds. */
    350                                 progress = sprintf(_n('%1$s · an image waits for another request, %2$s second at most', '%1$s · an image waits for another request, %2$s seconds at most', job.waiting, 'takopic'), progress, job.waiting.toLocaleString());
     350                                progress = sprintf(_n('%1$s · an image waits for another request, %2$s second at most', '%1$s · an image waits for another request, %2$s seconds at most', job.waiting, 'takopic'), progress, Screen.number(job.waiting));
    351351                        }
    352352                        // Pausing or cancelling, the control clicked tells it (setState()).
    … …  
    426426                        this.detail.replaceChildren(...(cancelled ? Job.stopped(job) : [...Job.took(texts.processed(job.done - job.failed - job.missing), job), '.']));
    427427                        /* translators: %s: the number of images. */
    428                         this.errorsCount.textContent = sprintf(_n('%s could not be optimised:', '%s could not be optimised:', job.failed, 'takopic'), job.failed.toLocaleString());
     428                        this.errorsCount.textContent = sprintf(_n('%s could not be optimised:', '%s could not be optimised:', job.failed, 'takopic'), Screen.number(job.failed));
    429429                        this.errors.hidden = !job.failed;
    430430                        /* translators: %s: the number of images. */
    431                         this.missing.textContent = sprintf(_n('%s had no original to restore, it stays compressed.', '%s had no original to restore, they stay compressed.', job.missing, 'takopic'), job.missing.toLocaleString());
     431                        this.missing.textContent = sprintf(_n('%s had no original to restore, it stays compressed.', '%s had no original to restore, they stay compressed.', job.missing, 'takopic'), Screen.number(job.missing));
    432432                        this.missing.hidden = !job.missing;
    433433
    … …  
    460460                static stopped(job) {
    461461                        /* translators: 1: the number of images processed, 2: the number of images. */
    462                         const stopped = Job.emphasise(_n('Stopped after %1$s of %2$s image', 'Stopped after %1$s of %2$s images', job.total, 'takopic'), job.done.toLocaleString(), job.total.toLocaleString());
     462                        const stopped = Job.emphasise(_n('Stopped after %1$s of %2$s image', 'Stopped after %1$s of %2$s images', job.total, 'takopic'), Screen.number(job.done), Screen.number(job.total));
    463463                        return [...Job.took(stopped, job), `. ${__('The images already processed stay processed.', 'takopic')}`];
    464464                }
  • takopic/tags/1.0.1/assets/js/components/Savings.js

    r3791227 r3791597  
    1616
    1717(() => {
    18         const {Component, Roll} = window.takopic;
     18        const {Component, Roll, Screen} = window.takopic;
    1919
    2020        class Savings extends Component {
    … …  
    7676                        const decimal = number.match(/([.,])\d$/)?.[1];
    7777                        const value = Number(number.replace(/\D/g, '')) / (decimal ? 10 : 1);
    78                         const format = (v) => before + (decimal ? v.toFixed(1).replace('.', decimal) : String(Math.round(v))) + after;
     78                        // The thousands separated as the server writes them, e.g. "1 023,5 Mo" in French.
     79                        const format = (v) => {
     80                                if (!decimal) {
     81                                        return before + Screen.number(v) + after;
     82                                }
     83                                const [whole, tenth] = v.toFixed(1).split('.');
     84                                return before + Screen.number(Number(whole)) + decimal + tenth + after;
     85                        };
    7986
    8087                        this.roll = new Roll(0, value, (v) => {
  • takopic/tags/1.0.1/assets/js/views/Screen.js

    r3791227 r3791597  
    1414        class Screen extends EventTarget {
    1515                /**
     16                 * The thousands separator of WordPress, see Ajax::localize().
     17                 */
     18                static #thousands = ',';
     19
     20                /**
    1621                 * @param {HTMLElement} el
    1722                 * @param {Array<{name: string, Class: Function}>} components A js- class, and the component built on its elements.
    18                  * @param {{ajaxurl: string, nonce: string}} settings
     23                 * @param {{ajaxurl: string, nonce: string, thousands: string}} settings
    1924                 */
    2025                constructor(el, components, settings) {
    2126                        super();
     27                        Screen.#thousands = settings.thousands ?? ',';
    2228                        this.el = el;
    2329                        this.settings = settings;
    … …  
    2632                        this.components = components.flatMap(({name, Class}) => [...el.querySelectorAll(`.${name}`)].map((element) => new Class(element, this)));
    2733                        this.components.forEach((component) => component.init());
     34                }
     35
     36                /**
     37                 * Formats a whole number as WordPress does
     38                 * (number_format_i18n()), e.g. "1 044" in French:
     39                 * as the numbers the server writes, whatever the
     40                 * language of the browser.
     41                 *
     42                 * @param {number} value
     43                 * @returns {string}
     44                 */
     45                static number(value) {
     46                        const digits = String(Math.abs(Math.round(value))).replace(/\B(?=(\d{3})+(?!\d))/g, Screen.#thousands);
     47                        return (value < 0 ? '-' : '') + digits;
    2848                }
    2949
  • takopic/tags/1.0.1/core/Admin/Ajax.php

    r3791227 r3791597  
    4040         * Gives a script of a screen what its requests need
    4141         * (see utils/Api.js): the URL of admin-ajax.php and
    42          * the nonce of the screen, with other data.
     42         * the nonce of the screen, with other data, and the
     43         * thousands separator of WordPress.
    4344         *
    4445         * @param string $handle
    … …  
    5051        public static function localize($handle, $name, $nonce, $data = [])
    5152        {
     53                global $wp_locale;
    5254                wp_localize_script($handle, $name, array_merge([
    5355                        'ajaxurl' => admin_url('admin-ajax.php'),
    5456                        'nonce' => wp_create_nonce($nonce),
     57                        // The numbers the scripts write look like those of number_format_i18n() (see views/Screen.js).
     58                        'thousands' => html_entity_decode($wp_locale->number_format['thousands_sep'], ENT_QUOTES, 'UTF-8'),
    5559                ], $data));
    5660        }
  • takopic/tags/1.0.1/core/Cli/Commands.php

    r3791227 r3791597  
    160160                $counter = 0;
    161161                $failed = 0;
    162                 foreach (Status::ids(null) as $id) {
    163                         WP_CLI::log(WP_CLI::colorize('%B' . __('Processing image:', 'takopic') . ' %n') . get_the_title($id));
     162                $ids = Status::ids(null);
     163                $total = count($ids);
     164                foreach ($ids as $index => $id) {
     165                        /* translators: 1: the number of the image, 2: the number of images. */
     166                        $progress = sprintf(__('Processing image %1$d/%2$d:', 'takopic'), $index + 1, $total);
     167                        /* translators: 1: the title of an image, 2: its ID. */
     168                        WP_CLI::log(WP_CLI::colorize('%B' . $progress . ' %n') . sprintf(__('%1$s (ID %2$d)', 'takopic'), get_the_title($id), $id));
    164169                        $done = Lock::run($id, function () use ($id, $regenerate, $settings, $force, $thumbnails_only) {
    165170                                $metadata = $regenerate ? Optimiser::regenerate($id) : null;
    … …  
    380385        private function print_welcome()
    381386        {
    382                 WP_CLI::line(WP_CLI::colorize('%G' . __('Welcome to the Takopic CLI...', 'takopic') . '%n' . PHP_EOL));
     387                // Not WP_CLI::line(): it echoes, held back until the end by an output buffer a theme or plugin may open.
     388                WP_CLI::log(WP_CLI::colorize('%G' . __('Welcome to the Takopic CLI...', 'takopic') . '%n' . PHP_EOL));
    383389        }
    384390
  • takopic/tags/1.0.1/core/Media/Optimiser.php

    r3791227 r3791597  
    381381                        $metadata = wp_get_attachment_metadata($id);
    382382                        $sizes = get_post_meta($id, Status::SIZES_KEY, true);
    383                         if ($regenerate || !Status::sizes_current($metadata) || ($sizes !== '' && $sizes !== Status::sizes_signature())) {
     383                        if ($regenerate || !Status::sizes_current($metadata, $id) || ($sizes !== '' && $sizes !== Status::sizes_signature())) {
    384384                                $metadata = self::regenerate($id);
    385385                                if ($metadata) {
  • takopic/tags/1.0.1/core/Media/Status.php

    r3791227 r3791597  
    6565
    6666        /**
     67         * Returns the thumbnail sizes WordPress makes for an
     68         * image: those registered by WordPress, the theme
     69         * and plugins, less those a plugin leaves out
     70         * through intermediate_image_sizes_advanced, as
     71         * WordPress does before making them. E.g. Gravity
     72         * Forms registers sizes it only makes for the images
     73         * of its own fields. Without an image, the sizes of
     74         * any image.
     75         *
     76         * @param array $metadata
     77         * @param int $id
     78         * @return array
     79         * @since 1.0.1
     80         */
     81        public static function subsizes($metadata = [], $id = 0)
     82        {
     83                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The filter of WordPress, applied as it does before making the thumbnails.
     84                $sizes = apply_filters('intermediate_image_sizes_advanced', wp_get_registered_image_subsizes(), is_array($metadata) ? $metadata : [], (int)$id);
     85                return is_array($sizes) ? $sizes : [];
     86        }
     87
     88        /**
    6789         * Returns a signature of the thumbnail sizes
    68          * registered by WordPress, the theme and plugins,
    69          * and of the maximum image size.
     90         * WordPress makes (see subsizes()), and of the
     91         * maximum image size: a size registered but never
     92         * made leaves it alone.
    7093         *
    7194         * @return string
    … …  
    7497        public static function sizes_signature()
    7598        {
    76                 return md5(wp_json_encode([wp_get_registered_image_subsizes(), Settings::get('max_size')]));
     99                return md5(wp_json_encode([self::subsizes(), Settings::get('max_size')]));
    77100        }
    78101
    79102        /**
    80103         * Determines if the thumbnails of an image match the
    81          * registered sizes: each size exists with the right
    82          * dimensions, unless the image is too small for it
    83          * (the same computation as WordPress). The
    84          * thumbnails of a scaled image are made from the
    85          * image uploaded, rounded differently: 1 pixel off.
     104         * sizes WordPress makes for it (see subsizes()):
     105         * each size exists with the right dimensions, unless
     106         * the image is too small for it (the same
     107         * computation as WordPress). The thumbnails of a
     108         * scaled image are made from the image uploaded,
     109         * rounded differently: 1 pixel off.
    86110         *
    87111         * @param array $metadata
     112         * @param int $id
    88113         * @return bool
    89114         * @since 1.0.0
    90115         */
    91         public static function sizes_current($metadata)
     116        public static function sizes_current($metadata, $id = 0)
    92117        {
    93118                if (empty($metadata['width']) || empty($metadata['height'])) {
    … …  
    95120                }
    96121                $sizes = isset($metadata['sizes']) ? $metadata['sizes'] : [];
    97                 foreach (wp_get_registered_image_subsizes() as $name => $size) {
     122                foreach (self::subsizes($metadata, $id) as $name => $size) {
    98123                        $expected = image_resize_dimensions($metadata['width'], $metadata['height'], $size['width'], $size['height'], $size['crop']);
    99124                        if (!$expected) {
    … …  
    117142        public static function stamp_sizes($id, $metadata)
    118143        {
    119                 if (is_array($metadata) && self::sizes_current($metadata)) {
     144                if (is_array($metadata) && self::sizes_current($metadata, $id)) {
    120145                        update_post_meta($id, self::SIZES_KEY, self::sizes_signature());
    121146                } else {
    … …  
    287312                        'orderby' => 'ID',
    288313                        'order' => 'ASC',
     314                        // Never from the query cache of WordPress: a persistent object cache may miss what another process
     315                        // changed, e.g. WP-CLI keyed elsewhere by W3 Total Cache, and keep counting it the old way.
     316                        'cache_results' => false,
    289317                ], $args);
    290318                if ($status === null) {
  • takopic/tags/1.0.1/readme.txt

    r3791227 r3791597  
    66Tested up to: 7.1
    77Requires PHP: 8.2
    8 Stable tag: 1.0.0
     8Stable tag: 1.0.1
    99License: GPLv3 or later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    119119== Changelog ==
    120120
    121 = 1.0.0 =
    122 * Renamed to Takopic, starting again from Squidge 0.1.4: no migration of its data.
    123 * The original images are kept in `uploads/takopic/originals-{key}/`, a folder whose URL cannot be guessed, and every optimisation starts again from them.
    124 * WebP and AVIF files keep the extension of their source (`image.jpg.webp`), in `uploads/takopic/formats/`, and can be served on the site with `<picture>` or rewrite rules.
    125 * Background optimisation of the media library, with pause and cancel, and re-optimisation when the settings or the thumbnail sizes change.
    126 * Clean up: remove the WebP and AVIF files, or restore the original images.
    127 * Status column in the media library and readable error notices.
    128 * CMYK JPG images, AVIF quality settings, CPU limits adjustable by filters.
    129 * Colour profiles kept in every format when the colours need them (Display P3, Adobe RGB).
    130 * WebP and AVIF versions of the photos stored sideways turned the right way up, even where WordPress did not turn them.
    131 * Requires WordPress 6.2 and PHP 8.2.
     121= 1.0.1 =
     122* Images no longer listed as "Older settings" for a thumbnail size a plugin registers but never makes: `Re-optimise outdated images` puts them back as optimised, without encoding them again.
     123* WP-CLI prints its welcome first, even when the theme or a plugin holds the output back.
     124* `wp takopic run` numbers the images it processes, with their ID, e.g. `Processing image 10/256: Sunset (ID 147)`.
     125* The counts of the images stay right with a persistent object cache that missed a change made elsewhere, e.g. by WP-CLI.
     126* The numbers the settings page updates are written as WordPress writes them, e.g. `1 044` in French, not `1,044`.
    132127
    133128The older versions, and those of Squidge before Takopic, are in the [full changelog](https://github.com/Striffly/takopic/blob/main/CHANGELOG.md).
  • takopic/tags/1.0.1/takopic.php

    r3791227 r3791597  
    88 * Author URI:      https://github.com/Striffly
    99 * Text Domain:     takopic
    10  * Version:         1.0.0
     10 * Version:         1.0.1
    1111 * Requires at least: 6.2
    1212 * Requires PHP:    8.2
    … …  
    2323
    2424// The version of Takopic, set by bun run release with the header above.
    25 define('TAKOPIC_VERSION', '1.0.0');
     25define('TAKOPIC_VERSION', '1.0.1');
    2626define('TAKOPIC_FILE', __FILE__);
    2727
  • takopic/tags/1.0.1/vendor-prefixed/autoload.php

    r3791227 r3791597  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05::getLoader();
     22return ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5::getLoader();
  • takopic/tags/1.0.1/vendor-prefixed/composer/autoload_real.php

    r3791227 r3791597  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05
     5class ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5
    66{
    77    private static $loader;
    … …  
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Takopic\Vendor\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05::getInitializer($loader));
     32        call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::getInitializer($loader));
    3333
    3434        $loader->setClassMapAuthoritative(true);
  • takopic/tags/1.0.1/vendor-prefixed/composer/autoload_static.php

    r3791227 r3791597  
    55namespace Takopic\Vendor\Composer\Autoload;
    66
    7 class ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05
     7class ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5
    88{
    99    public static $classMap = array (
    … …  
    1616    {
    1717        return \Closure::bind(function () use ($loader) {
    18             $loader->classMap = ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05::$classMap;
     18            $loader->classMap = ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::$classMap;
    1919
    2020        }, null, ClassLoader::class);
  • takopic/tags/1.0.1/vendor-prefixed/composer/installed.php

    r3791227 r3791597  
    33  array (
    44    'name' => 'striffly/takopic',
    5     'pretty_version' => '1.0.0',
    6     'version' => '1.0.0.0',
    7     'reference' => '042fd52311d1c62d1567bb7a660c51d2366b2062',
     5    'pretty_version' => '1.0.1',
     6    'version' => '1.0.1.0',
     7    'reference' => '67838e166d9935705d11cf72c9951e383810cd67',
    88    'type' => 'wordpress-plugin',
    99    'install_path' => __DIR__ . '/../',
  • takopic/trunk/assets/js/components/Count.js

    r3791227 r3791597  
    99
    1010(() => {
    11         const {Component, Roll} = window.takopic;
     11        const {Component, Roll, Screen} = window.takopic;
    1212
    1313        class Count extends Component {
    … …  
    4141                 */
    4242                show(value) {
    43                         this.el.textContent = value.toLocaleString();
     43                        this.el.textContent = Screen.number(value);
    4444                }
    4545
  • takopic/trunk/assets/js/components/DeactivationWarning.js

    r3791227 r3791597  
    1515(() => {
    1616        const {_n, sprintf} = wp.i18n;
    17         const {Component} = window.takopic;
     17        const {Component, Screen} = window.takopic;
    1818
    1919        class DeactivationWarning extends Component {
    … …  
    6565                        }
    6666                        /* translators: %s: the number of images. */
    67                         this.images.textContent = sprintf(_n('%s image', '%s images', images, 'takopic'), images.toLocaleString());
     67                        this.images.textContent = sprintf(_n('%s image', '%s images', images, 'takopic'), Screen.number(images));
    6868                        this.size.textContent = size ? ` (${size})` : '';
    6969                        this.originals.hidden = !images;
  • takopic/trunk/assets/js/components/Job.js

    r3791227 r3791597  
    1818(() => {
    1919        const {__, _n, sprintf} = wp.i18n;
    20         const {Component, Confetti} = window.takopic;
     20        const {Component, Confetti, Screen} = window.takopic;
    2121
    2222        class Job extends Component {
    … …  
    6262                                cancelled: __('Optimisation cancelled', 'takopic'),
    6363                                /* translators: %s: the number of images. */
    64                                 processed: (count) => Job.emphasise(_n('%s image optimised', '%s images optimised', count, 'takopic'), count.toLocaleString()),
     64                                processed: (count) => Job.emphasise(_n('%s image optimised', '%s images optimised', count, 'takopic'), Screen.number(count)),
    6565                        },
    6666                        remove_formats: {
    … …  
    6969                                cancelled: __('Removal cancelled', 'takopic'),
    7070                                /* translators: %s: the number of images. */
    71                                 processed: (count) => Job.emphasise(_n('%s image cleaned up', '%s images cleaned up', count, 'takopic'), count.toLocaleString()),
     71                                processed: (count) => Job.emphasise(_n('%s image cleaned up', '%s images cleaned up', count, 'takopic'), Screen.number(count)),
    7272                        },
    7373                        restore: {
    … …  
    7676                                cancelled: __('Restore cancelled', 'takopic'),
    7777                                /* translators: %s: the number of images. */
    78                                 processed: (count) => Job.emphasise(_n('%s image restored', '%s images restored', count, 'takopic'), count.toLocaleString()),
     78                                processed: (count) => Job.emphasise(_n('%s image restored', '%s images restored', count, 'takopic'), Screen.number(count)),
    7979                        },
    8080                };
    … …  
    340340
    341341                        /* translators: 1: the number of images processed, 2: the number of images. */
    342                         let progress = sprintf(_n('%1$s / %2$s image', '%1$s / %2$s images', job.total, 'takopic'), job.done.toLocaleString(), job.total.toLocaleString());
     342                        let progress = sprintf(_n('%1$s / %2$s image', '%1$s / %2$s images', job.total, 'takopic'), Screen.number(job.done), Screen.number(job.total));
    343343                        if (job.failed) {
    344344                                /* translators: 1: the progress, e.g. 3 / 12 images, 2: the number of images with errors. */
    345                                 progress = sprintf(_n('%1$s, %2$s with errors', '%1$s, %2$s with errors', job.failed, 'takopic'), progress, job.failed.toLocaleString());
     345                                progress = sprintf(_n('%1$s, %2$s with errors', '%1$s, %2$s with errors', job.failed, 'takopic'), progress, Screen.number(job.failed));
    346346                        }
    347347                        if (job.waiting) {
    348348                                // Held by another request, e.g. a worker PHP stopped: its lock goes stale within a minute.
    349349                                /* translators: 1: the progress, e.g. 3 / 12 images, 2: a number of seconds. */
    350                                 progress = sprintf(_n('%1$s · an image waits for another request, %2$s second at most', '%1$s · an image waits for another request, %2$s seconds at most', job.waiting, 'takopic'), progress, job.waiting.toLocaleString());
     350                                progress = sprintf(_n('%1$s · an image waits for another request, %2$s second at most', '%1$s · an image waits for another request, %2$s seconds at most', job.waiting, 'takopic'), progress, Screen.number(job.waiting));
    351351                        }
    352352                        // Pausing or cancelling, the control clicked tells it (setState()).
    … …  
    426426                        this.detail.replaceChildren(...(cancelled ? Job.stopped(job) : [...Job.took(texts.processed(job.done - job.failed - job.missing), job), '.']));
    427427                        /* translators: %s: the number of images. */
    428                         this.errorsCount.textContent = sprintf(_n('%s could not be optimised:', '%s could not be optimised:', job.failed, 'takopic'), job.failed.toLocaleString());
     428                        this.errorsCount.textContent = sprintf(_n('%s could not be optimised:', '%s could not be optimised:', job.failed, 'takopic'), Screen.number(job.failed));
    429429                        this.errors.hidden = !job.failed;
    430430                        /* translators: %s: the number of images. */
    431                         this.missing.textContent = sprintf(_n('%s had no original to restore, it stays compressed.', '%s had no original to restore, they stay compressed.', job.missing, 'takopic'), job.missing.toLocaleString());
     431                        this.missing.textContent = sprintf(_n('%s had no original to restore, it stays compressed.', '%s had no original to restore, they stay compressed.', job.missing, 'takopic'), Screen.number(job.missing));
    432432                        this.missing.hidden = !job.missing;
    433433
    … …  
    460460                static stopped(job) {
    461461                        /* translators: 1: the number of images processed, 2: the number of images. */
    462                         const stopped = Job.emphasise(_n('Stopped after %1$s of %2$s image', 'Stopped after %1$s of %2$s images', job.total, 'takopic'), job.done.toLocaleString(), job.total.toLocaleString());
     462                        const stopped = Job.emphasise(_n('Stopped after %1$s of %2$s image', 'Stopped after %1$s of %2$s images', job.total, 'takopic'), Screen.number(job.done), Screen.number(job.total));
    463463                        return [...Job.took(stopped, job), `. ${__('The images already processed stay processed.', 'takopic')}`];
    464464                }
  • takopic/trunk/assets/js/components/Savings.js

    r3791227 r3791597  
    1616
    1717(() => {
    18         const {Component, Roll} = window.takopic;
     18        const {Component, Roll, Screen} = window.takopic;
    1919
    2020        class Savings extends Component {
    … …  
    7676                        const decimal = number.match(/([.,])\d$/)?.[1];
    7777                        const value = Number(number.replace(/\D/g, '')) / (decimal ? 10 : 1);
    78                         const format = (v) => before + (decimal ? v.toFixed(1).replace('.', decimal) : String(Math.round(v))) + after;
     78                        // The thousands separated as the server writes them, e.g. "1 023,5 Mo" in French.
     79                        const format = (v) => {
     80                                if (!decimal) {
     81                                        return before + Screen.number(v) + after;
     82                                }
     83                                const [whole, tenth] = v.toFixed(1).split('.');
     84                                return before + Screen.number(Number(whole)) + decimal + tenth + after;
     85                        };
    7986
    8087                        this.roll = new Roll(0, value, (v) => {
  • takopic/trunk/assets/js/views/Screen.js

    r3791227 r3791597  
    1414        class Screen extends EventTarget {
    1515                /**
     16                 * The thousands separator of WordPress, see Ajax::localize().
     17                 */
     18                static #thousands = ',';
     19
     20                /**
    1621                 * @param {HTMLElement} el
    1722                 * @param {Array<{name: string, Class: Function}>} components A js- class, and the component built on its elements.
    18                  * @param {{ajaxurl: string, nonce: string}} settings
     23                 * @param {{ajaxurl: string, nonce: string, thousands: string}} settings
    1924                 */
    2025                constructor(el, components, settings) {
    2126                        super();
     27                        Screen.#thousands = settings.thousands ?? ',';
    2228                        this.el = el;
    2329                        this.settings = settings;
    … …  
    2632                        this.components = components.flatMap(({name, Class}) => [...el.querySelectorAll(`.${name}`)].map((element) => new Class(element, this)));
    2733                        this.components.forEach((component) => component.init());
     34                }
     35
     36                /**
     37                 * Formats a whole number as WordPress does
     38                 * (number_format_i18n()), e.g. "1 044" in French:
     39                 * as the numbers the server writes, whatever the
     40                 * language of the browser.
     41                 *
     42                 * @param {number} value
     43                 * @returns {string}
     44                 */
     45                static number(value) {
     46                        const digits = String(Math.abs(Math.round(value))).replace(/\B(?=(\d{3})+(?!\d))/g, Screen.#thousands);
     47                        return (value < 0 ? '-' : '') + digits;
    2848                }
    2949
  • takopic/trunk/core/Admin/Ajax.php

    r3791227 r3791597  
    4040         * Gives a script of a screen what its requests need
    4141         * (see utils/Api.js): the URL of admin-ajax.php and
    42          * the nonce of the screen, with other data.
     42         * the nonce of the screen, with other data, and the
     43         * thousands separator of WordPress.
    4344         *
    4445         * @param string $handle
    … …  
    5051        public static function localize($handle, $name, $nonce, $data = [])
    5152        {
     53                global $wp_locale;
    5254                wp_localize_script($handle, $name, array_merge([
    5355                        'ajaxurl' => admin_url('admin-ajax.php'),
    5456                        'nonce' => wp_create_nonce($nonce),
     57                        // The numbers the scripts write look like those of number_format_i18n() (see views/Screen.js).
     58                        'thousands' => html_entity_decode($wp_locale->number_format['thousands_sep'], ENT_QUOTES, 'UTF-8'),
    5559                ], $data));
    5660        }
  • takopic/trunk/core/Cli/Commands.php

    r3791227 r3791597  
    160160                $counter = 0;
    161161                $failed = 0;
    162                 foreach (Status::ids(null) as $id) {
    163                         WP_CLI::log(WP_CLI::colorize('%B' . __('Processing image:', 'takopic') . ' %n') . get_the_title($id));
     162                $ids = Status::ids(null);
     163                $total = count($ids);
     164                foreach ($ids as $index => $id) {
     165                        /* translators: 1: the number of the image, 2: the number of images. */
     166                        $progress = sprintf(__('Processing image %1$d/%2$d:', 'takopic'), $index + 1, $total);
     167                        /* translators: 1: the title of an image, 2: its ID. */
     168                        WP_CLI::log(WP_CLI::colorize('%B' . $progress . ' %n') . sprintf(__('%1$s (ID %2$d)', 'takopic'), get_the_title($id), $id));
    164169                        $done = Lock::run($id, function () use ($id, $regenerate, $settings, $force, $thumbnails_only) {
    165170                                $metadata = $regenerate ? Optimiser::regenerate($id) : null;
    … …  
    380385        private function print_welcome()
    381386        {
    382                 WP_CLI::line(WP_CLI::colorize('%G' . __('Welcome to the Takopic CLI...', 'takopic') . '%n' . PHP_EOL));
     387                // Not WP_CLI::line(): it echoes, held back until the end by an output buffer a theme or plugin may open.
     388                WP_CLI::log(WP_CLI::colorize('%G' . __('Welcome to the Takopic CLI...', 'takopic') . '%n' . PHP_EOL));
    383389        }
    384390
  • takopic/trunk/core/Media/Optimiser.php

    r3791227 r3791597  
    381381                        $metadata = wp_get_attachment_metadata($id);
    382382                        $sizes = get_post_meta($id, Status::SIZES_KEY, true);
    383                         if ($regenerate || !Status::sizes_current($metadata) || ($sizes !== '' && $sizes !== Status::sizes_signature())) {
     383                        if ($regenerate || !Status::sizes_current($metadata, $id) || ($sizes !== '' && $sizes !== Status::sizes_signature())) {
    384384                                $metadata = self::regenerate($id);
    385385                                if ($metadata) {
  • takopic/trunk/core/Media/Status.php

    r3791227 r3791597  
    6565
    6666        /**
     67         * Returns the thumbnail sizes WordPress makes for an
     68         * image: those registered by WordPress, the theme
     69         * and plugins, less those a plugin leaves out
     70         * through intermediate_image_sizes_advanced, as
     71         * WordPress does before making them. E.g. Gravity
     72         * Forms registers sizes it only makes for the images
     73         * of its own fields. Without an image, the sizes of
     74         * any image.
     75         *
     76         * @param array $metadata
     77         * @param int $id
     78         * @return array
     79         * @since 1.0.1
     80         */
     81        public static function subsizes($metadata = [], $id = 0)
     82        {
     83                // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- The filter of WordPress, applied as it does before making the thumbnails.
     84                $sizes = apply_filters('intermediate_image_sizes_advanced', wp_get_registered_image_subsizes(), is_array($metadata) ? $metadata : [], (int)$id);
     85                return is_array($sizes) ? $sizes : [];
     86        }
     87
     88        /**
    6789         * Returns a signature of the thumbnail sizes
    68          * registered by WordPress, the theme and plugins,
    69          * and of the maximum image size.
     90         * WordPress makes (see subsizes()), and of the
     91         * maximum image size: a size registered but never
     92         * made leaves it alone.
    7093         *
    7194         * @return string
    … …  
    7497        public static function sizes_signature()
    7598        {
    76                 return md5(wp_json_encode([wp_get_registered_image_subsizes(), Settings::get('max_size')]));
     99                return md5(wp_json_encode([self::subsizes(), Settings::get('max_size')]));
    77100        }
    78101
    79102        /**
    80103         * Determines if the thumbnails of an image match the
    81          * registered sizes: each size exists with the right
    82          * dimensions, unless the image is too small for it
    83          * (the same computation as WordPress). The
    84          * thumbnails of a scaled image are made from the
    85          * image uploaded, rounded differently: 1 pixel off.
     104         * sizes WordPress makes for it (see subsizes()):
     105         * each size exists with the right dimensions, unless
     106         * the image is too small for it (the same
     107         * computation as WordPress). The thumbnails of a
     108         * scaled image are made from the image uploaded,
     109         * rounded differently: 1 pixel off.
    86110         *
    87111         * @param array $metadata
     112         * @param int $id
    88113         * @return bool
    89114         * @since 1.0.0
    90115         */
    91         public static function sizes_current($metadata)
     116        public static function sizes_current($metadata, $id = 0)
    92117        {
    93118                if (empty($metadata['width']) || empty($metadata['height'])) {
    … …  
    95120                }
    96121                $sizes = isset($metadata['sizes']) ? $metadata['sizes'] : [];
    97                 foreach (wp_get_registered_image_subsizes() as $name => $size) {
     122                foreach (self::subsizes($metadata, $id) as $name => $size) {
    98123                        $expected = image_resize_dimensions($metadata['width'], $metadata['height'], $size['width'], $size['height'], $size['crop']);
    99124                        if (!$expected) {
    … …  
    117142        public static function stamp_sizes($id, $metadata)
    118143        {
    119                 if (is_array($metadata) && self::sizes_current($metadata)) {
     144                if (is_array($metadata) && self::sizes_current($metadata, $id)) {
    120145                        update_post_meta($id, self::SIZES_KEY, self::sizes_signature());
    121146                } else {
    … …  
    287312                        'orderby' => 'ID',
    288313                        'order' => 'ASC',
     314                        // Never from the query cache of WordPress: a persistent object cache may miss what another process
     315                        // changed, e.g. WP-CLI keyed elsewhere by W3 Total Cache, and keep counting it the old way.
     316                        'cache_results' => false,
    289317                ], $args);
    290318                if ($status === null) {
  • takopic/trunk/readme.txt

    r3791227 r3791597  
    66Tested up to: 7.1
    77Requires PHP: 8.2
    8 Stable tag: 1.0.0
     8Stable tag: 1.0.1
    99License: GPLv3 or later
    1010License URI: https://www.gnu.org/licenses/gpl-3.0.html
    … …  
    119119== Changelog ==
    120120
    121 = 1.0.0 =
    122 * Renamed to Takopic, starting again from Squidge 0.1.4: no migration of its data.
    123 * The original images are kept in `uploads/takopic/originals-{key}/`, a folder whose URL cannot be guessed, and every optimisation starts again from them.
    124 * WebP and AVIF files keep the extension of their source (`image.jpg.webp`), in `uploads/takopic/formats/`, and can be served on the site with `<picture>` or rewrite rules.
    125 * Background optimisation of the media library, with pause and cancel, and re-optimisation when the settings or the thumbnail sizes change.
    126 * Clean up: remove the WebP and AVIF files, or restore the original images.
    127 * Status column in the media library and readable error notices.
    128 * CMYK JPG images, AVIF quality settings, CPU limits adjustable by filters.
    129 * Colour profiles kept in every format when the colours need them (Display P3, Adobe RGB).
    130 * WebP and AVIF versions of the photos stored sideways turned the right way up, even where WordPress did not turn them.
    131 * Requires WordPress 6.2 and PHP 8.2.
     121= 1.0.1 =
     122* Images no longer listed as "Older settings" for a thumbnail size a plugin registers but never makes: `Re-optimise outdated images` puts them back as optimised, without encoding them again.
     123* WP-CLI prints its welcome first, even when the theme or a plugin holds the output back.
     124* `wp takopic run` numbers the images it processes, with their ID, e.g. `Processing image 10/256: Sunset (ID 147)`.
     125* The counts of the images stay right with a persistent object cache that missed a change made elsewhere, e.g. by WP-CLI.
     126* The numbers the settings page updates are written as WordPress writes them, e.g. `1 044` in French, not `1,044`.
    132127
    133128The older versions, and those of Squidge before Takopic, are in the [full changelog](https://github.com/Striffly/takopic/blob/main/CHANGELOG.md).
  • takopic/trunk/takopic.php

    r3791227 r3791597  
    88 * Author URI:      https://github.com/Striffly
    99 * Text Domain:     takopic
    10  * Version:         1.0.0
     10 * Version:         1.0.1
    1111 * Requires at least: 6.2
    1212 * Requires PHP:    8.2
    … …  
    2323
    2424// The version of Takopic, set by bun run release with the header above.
    25 define('TAKOPIC_VERSION', '1.0.0');
     25define('TAKOPIC_VERSION', '1.0.1');
    2626define('TAKOPIC_FILE', __FILE__);
    2727
  • takopic/trunk/vendor-prefixed/autoload.php

    r3791227 r3791597  
    2020require_once __DIR__ . '/composer/autoload_real.php';
    2121
    22 return ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05::getLoader();
     22return ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5::getLoader();
  • takopic/trunk/vendor-prefixed/composer/autoload_real.php

    r3791227 r3791597  
    33// autoload_real.php @generated by Composer
    44
    5 class ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05
     5class ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5
    66{
    77    private static $loader;
    … …  
    2525        require __DIR__ . '/platform_check.php';
    2626
    27         spl_autoload_register(array('ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'), true, true);
     27        spl_autoload_register(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'), true, true);
    2828        self::$loader = $loader = new \Takopic\Vendor\Composer\Autoload\ClassLoader(\dirname(__DIR__));
    29         spl_autoload_unregister(array('ComposerAutoloaderInit0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'));
     29        spl_autoload_unregister(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'));
    3030
    3131        require __DIR__ . '/autoload_static.php';
    32         call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05::getInitializer($loader));
     32        call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::getInitializer($loader));
    3333
    3434        $loader->setClassMapAuthoritative(true);
  • takopic/trunk/vendor-prefixed/composer/autoload_static.php

    r3791227 r3791597  
    55namespace Takopic\Vendor\Composer\Autoload;
    66
    7 class ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05
     7class ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5
    88{
    99    public static $classMap = array (
    … …  
    1616    {
    1717        return \Closure::bind(function () use ($loader) {
    18             $loader->classMap = ComposerStaticInit0933e086ea6d51c1829a5766a6c1ba05::$classMap;
     18            $loader->classMap = ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::$classMap;
    1919
    2020        }, null, ClassLoader::class);
  • takopic/trunk/vendor-prefixed/composer/installed.php

    r3791227 r3791597  
    33  array (
    44    'name' => 'striffly/takopic',
    5     'pretty_version' => '1.0.0',
    6     'version' => '1.0.0.0',
    7     'reference' => '042fd52311d1c62d1567bb7a660c51d2366b2062',
     5    'pretty_version' => '1.0.1',
     6    'version' => '1.0.1.0',
     7    'reference' => '67838e166d9935705d11cf72c9951e383810cd67',
    88    'type' => 'wordpress-plugin',
    99    'install_path' => __DIR__ . '/../',
Note: See TracChangeset for help on using the changeset viewer.