Changeset 3791597
- Timestamp:
- 11/08/2026 10:58:25 PM (less than one hour ago)
- Location:
- takopic
- Files:
-
- 30 edited
- 1 copied
-
tags/1.0.1 (copied) (copied from takopic/trunk )
-
tags/1.0.1/assets/js/components/Count.js (modified) (2 diffs)
-
tags/1.0.1/assets/js/components/DeactivationWarning.js (modified) (2 diffs)
-
tags/1.0.1/assets/js/components/Job.js (modified) (7 diffs)
-
tags/1.0.1/assets/js/components/Savings.js (modified) (2 diffs)
-
tags/1.0.1/assets/js/views/Screen.js (modified) (2 diffs)
-
tags/1.0.1/core/Admin/Ajax.php (modified) (2 diffs)
-
tags/1.0.1/core/Cli/Commands.php (modified) (2 diffs)
-
tags/1.0.1/core/Media/Optimiser.php (modified) (1 diff)
-
tags/1.0.1/core/Media/Status.php (modified) (5 diffs)
-
tags/1.0.1/readme.txt (modified) (2 diffs)
-
tags/1.0.1/takopic.php (modified) (2 diffs)
-
tags/1.0.1/vendor-prefixed/autoload.php (modified) (1 diff)
-
tags/1.0.1/vendor-prefixed/composer/autoload_real.php (modified) (2 diffs)
-
tags/1.0.1/vendor-prefixed/composer/autoload_static.php (modified) (2 diffs)
-
tags/1.0.1/vendor-prefixed/composer/installed.php (modified) (1 diff)
-
trunk/assets/js/components/Count.js (modified) (2 diffs)
-
trunk/assets/js/components/DeactivationWarning.js (modified) (2 diffs)
-
trunk/assets/js/components/Job.js (modified) (7 diffs)
-
trunk/assets/js/components/Savings.js (modified) (2 diffs)
-
trunk/assets/js/views/Screen.js (modified) (2 diffs)
-
trunk/core/Admin/Ajax.php (modified) (2 diffs)
-
trunk/core/Cli/Commands.php (modified) (2 diffs)
-
trunk/core/Media/Optimiser.php (modified) (1 diff)
-
trunk/core/Media/Status.php (modified) (5 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/takopic.php (modified) (2 diffs)
-
trunk/vendor-prefixed/autoload.php (modified) (1 diff)
-
trunk/vendor-prefixed/composer/autoload_real.php (modified) (2 diffs)
-
trunk/vendor-prefixed/composer/autoload_static.php (modified) (2 diffs)
-
trunk/vendor-prefixed/composer/installed.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
takopic/tags/1.0.1/assets/js/components/Count.js
r3791227 r3791597 9 9 10 10 (() => { 11 const {Component, Roll } = window.takopic;11 const {Component, Roll, Screen} = window.takopic; 12 12 13 13 class Count extends Component { … … 41 41 */ 42 42 show(value) { 43 this.el.textContent = value.toLocaleString();43 this.el.textContent = Screen.number(value); 44 44 } 45 45 -
takopic/tags/1.0.1/assets/js/components/DeactivationWarning.js
r3791227 r3791597 15 15 (() => { 16 16 const {_n, sprintf} = wp.i18n; 17 const {Component } = window.takopic;17 const {Component, Screen} = window.takopic; 18 18 19 19 class DeactivationWarning extends Component { … … 65 65 } 66 66 /* 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)); 68 68 this.size.textContent = size ? ` (${size})` : ''; 69 69 this.originals.hidden = !images; -
takopic/tags/1.0.1/assets/js/components/Job.js
r3791227 r3791597 18 18 (() => { 19 19 const {__, _n, sprintf} = wp.i18n; 20 const {Component, Confetti } = window.takopic;20 const {Component, Confetti, Screen} = window.takopic; 21 21 22 22 class Job extends Component { … … 62 62 cancelled: __('Optimisation cancelled', 'takopic'), 63 63 /* 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)), 65 65 }, 66 66 remove_formats: { … … 69 69 cancelled: __('Removal cancelled', 'takopic'), 70 70 /* 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)), 72 72 }, 73 73 restore: { … … 76 76 cancelled: __('Restore cancelled', 'takopic'), 77 77 /* 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)), 79 79 }, 80 80 }; … … 340 340 341 341 /* 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)); 343 343 if (job.failed) { 344 344 /* 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)); 346 346 } 347 347 if (job.waiting) { 348 348 // Held by another request, e.g. a worker PHP stopped: its lock goes stale within a minute. 349 349 /* 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)); 351 351 } 352 352 // Pausing or cancelling, the control clicked tells it (setState()). … … 426 426 this.detail.replaceChildren(...(cancelled ? Job.stopped(job) : [...Job.took(texts.processed(job.done - job.failed - job.missing), job), '.'])); 427 427 /* 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)); 429 429 this.errors.hidden = !job.failed; 430 430 /* 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)); 432 432 this.missing.hidden = !job.missing; 433 433 … … 460 460 static stopped(job) { 461 461 /* 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)); 463 463 return [...Job.took(stopped, job), `. ${__('The images already processed stay processed.', 'takopic')}`]; 464 464 } -
takopic/tags/1.0.1/assets/js/components/Savings.js
r3791227 r3791597 16 16 17 17 (() => { 18 const {Component, Roll } = window.takopic;18 const {Component, Roll, Screen} = window.takopic; 19 19 20 20 class Savings extends Component { … … 76 76 const decimal = number.match(/([.,])\d$/)?.[1]; 77 77 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 }; 79 86 80 87 this.roll = new Roll(0, value, (v) => { -
takopic/tags/1.0.1/assets/js/views/Screen.js
r3791227 r3791597 14 14 class Screen extends EventTarget { 15 15 /** 16 * The thousands separator of WordPress, see Ajax::localize(). 17 */ 18 static #thousands = ','; 19 20 /** 16 21 * @param {HTMLElement} el 17 22 * @param {Array<{name: string, Class: Function}>} components A js- class, and the component built on its elements. 18 * @param {{ajaxurl: string, nonce: string }} settings23 * @param {{ajaxurl: string, nonce: string, thousands: string}} settings 19 24 */ 20 25 constructor(el, components, settings) { 21 26 super(); 27 Screen.#thousands = settings.thousands ?? ','; 22 28 this.el = el; 23 29 this.settings = settings; … … 26 32 this.components = components.flatMap(({name, Class}) => [...el.querySelectorAll(`.${name}`)].map((element) => new Class(element, this))); 27 33 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; 28 48 } 29 49 -
takopic/tags/1.0.1/core/Admin/Ajax.php
r3791227 r3791597 40 40 * Gives a script of a screen what its requests need 41 41 * (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. 43 44 * 44 45 * @param string $handle … … 50 51 public static function localize($handle, $name, $nonce, $data = []) 51 52 { 53 global $wp_locale; 52 54 wp_localize_script($handle, $name, array_merge([ 53 55 'ajaxurl' => admin_url('admin-ajax.php'), 54 56 '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'), 55 59 ], $data)); 56 60 } -
takopic/tags/1.0.1/core/Cli/Commands.php
r3791227 r3791597 160 160 $counter = 0; 161 161 $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)); 164 169 $done = Lock::run($id, function () use ($id, $regenerate, $settings, $force, $thumbnails_only) { 165 170 $metadata = $regenerate ? Optimiser::regenerate($id) : null; … … 380 385 private function print_welcome() 381 386 { 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)); 383 389 } 384 390 -
takopic/tags/1.0.1/core/Media/Optimiser.php
r3791227 r3791597 381 381 $metadata = wp_get_attachment_metadata($id); 382 382 $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())) { 384 384 $metadata = self::regenerate($id); 385 385 if ($metadata) { -
takopic/tags/1.0.1/core/Media/Status.php
r3791227 r3791597 65 65 66 66 /** 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 /** 67 89 * 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. 70 93 * 71 94 * @return string … … 74 97 public static function sizes_signature() 75 98 { 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')])); 77 100 } 78 101 79 102 /** 80 103 * 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. 86 110 * 87 111 * @param array $metadata 112 * @param int $id 88 113 * @return bool 89 114 * @since 1.0.0 90 115 */ 91 public static function sizes_current($metadata )116 public static function sizes_current($metadata, $id = 0) 92 117 { 93 118 if (empty($metadata['width']) || empty($metadata['height'])) { … … 95 120 } 96 121 $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) { 98 123 $expected = image_resize_dimensions($metadata['width'], $metadata['height'], $size['width'], $size['height'], $size['crop']); 99 124 if (!$expected) { … … 117 142 public static function stamp_sizes($id, $metadata) 118 143 { 119 if (is_array($metadata) && self::sizes_current($metadata )) {144 if (is_array($metadata) && self::sizes_current($metadata, $id)) { 120 145 update_post_meta($id, self::SIZES_KEY, self::sizes_signature()); 121 146 } else { … … 287 312 'orderby' => 'ID', 288 313 '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, 289 317 ], $args); 290 318 if ($status === null) { -
takopic/tags/1.0.1/readme.txt
r3791227 r3791597 6 6 Tested up to: 7.1 7 7 Requires PHP: 8.2 8 Stable tag: 1.0. 08 Stable tag: 1.0.1 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 119 119 == Changelog == 120 120 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`. 132 127 133 128 The 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 8 8 * Author URI: https://github.com/Striffly 9 9 * Text Domain: takopic 10 * Version: 1.0. 010 * Version: 1.0.1 11 11 * Requires at least: 6.2 12 12 * Requires PHP: 8.2 … … 23 23 24 24 // The version of Takopic, set by bun run release with the header above. 25 define('TAKOPIC_VERSION', '1.0. 0');25 define('TAKOPIC_VERSION', '1.0.1'); 26 26 define('TAKOPIC_FILE', __FILE__); 27 27 -
takopic/tags/1.0.1/vendor-prefixed/autoload.php
r3791227 r3791597 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05::getLoader();22 return ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5::getLoader(); -
takopic/tags/1.0.1/vendor-prefixed/composer/autoload_real.php
r3791227 r3791597 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba055 class ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Takopic\Vendor\Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba05::getInitializer($loader));32 call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::getInitializer($loader)); 33 33 34 34 $loader->setClassMapAuthoritative(true); -
takopic/tags/1.0.1/vendor-prefixed/composer/autoload_static.php
r3791227 r3791597 5 5 namespace Takopic\Vendor\Composer\Autoload; 6 6 7 class ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba057 class ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5 8 8 { 9 9 public static $classMap = array ( … … 16 16 { 17 17 return \Closure::bind(function () use ($loader) { 18 $loader->classMap = ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba05::$classMap;18 $loader->classMap = ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::$classMap; 19 19 20 20 }, null, ClassLoader::class); -
takopic/tags/1.0.1/vendor-prefixed/composer/installed.php
r3791227 r3791597 3 3 array ( 4 4 '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', 8 8 'type' => 'wordpress-plugin', 9 9 'install_path' => __DIR__ . '/../', -
takopic/trunk/assets/js/components/Count.js
r3791227 r3791597 9 9 10 10 (() => { 11 const {Component, Roll } = window.takopic;11 const {Component, Roll, Screen} = window.takopic; 12 12 13 13 class Count extends Component { … … 41 41 */ 42 42 show(value) { 43 this.el.textContent = value.toLocaleString();43 this.el.textContent = Screen.number(value); 44 44 } 45 45 -
takopic/trunk/assets/js/components/DeactivationWarning.js
r3791227 r3791597 15 15 (() => { 16 16 const {_n, sprintf} = wp.i18n; 17 const {Component } = window.takopic;17 const {Component, Screen} = window.takopic; 18 18 19 19 class DeactivationWarning extends Component { … … 65 65 } 66 66 /* 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)); 68 68 this.size.textContent = size ? ` (${size})` : ''; 69 69 this.originals.hidden = !images; -
takopic/trunk/assets/js/components/Job.js
r3791227 r3791597 18 18 (() => { 19 19 const {__, _n, sprintf} = wp.i18n; 20 const {Component, Confetti } = window.takopic;20 const {Component, Confetti, Screen} = window.takopic; 21 21 22 22 class Job extends Component { … … 62 62 cancelled: __('Optimisation cancelled', 'takopic'), 63 63 /* 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)), 65 65 }, 66 66 remove_formats: { … … 69 69 cancelled: __('Removal cancelled', 'takopic'), 70 70 /* 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)), 72 72 }, 73 73 restore: { … … 76 76 cancelled: __('Restore cancelled', 'takopic'), 77 77 /* 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)), 79 79 }, 80 80 }; … … 340 340 341 341 /* 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)); 343 343 if (job.failed) { 344 344 /* 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)); 346 346 } 347 347 if (job.waiting) { 348 348 // Held by another request, e.g. a worker PHP stopped: its lock goes stale within a minute. 349 349 /* 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)); 351 351 } 352 352 // Pausing or cancelling, the control clicked tells it (setState()). … … 426 426 this.detail.replaceChildren(...(cancelled ? Job.stopped(job) : [...Job.took(texts.processed(job.done - job.failed - job.missing), job), '.'])); 427 427 /* 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)); 429 429 this.errors.hidden = !job.failed; 430 430 /* 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)); 432 432 this.missing.hidden = !job.missing; 433 433 … … 460 460 static stopped(job) { 461 461 /* 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)); 463 463 return [...Job.took(stopped, job), `. ${__('The images already processed stay processed.', 'takopic')}`]; 464 464 } -
takopic/trunk/assets/js/components/Savings.js
r3791227 r3791597 16 16 17 17 (() => { 18 const {Component, Roll } = window.takopic;18 const {Component, Roll, Screen} = window.takopic; 19 19 20 20 class Savings extends Component { … … 76 76 const decimal = number.match(/([.,])\d$/)?.[1]; 77 77 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 }; 79 86 80 87 this.roll = new Roll(0, value, (v) => { -
takopic/trunk/assets/js/views/Screen.js
r3791227 r3791597 14 14 class Screen extends EventTarget { 15 15 /** 16 * The thousands separator of WordPress, see Ajax::localize(). 17 */ 18 static #thousands = ','; 19 20 /** 16 21 * @param {HTMLElement} el 17 22 * @param {Array<{name: string, Class: Function}>} components A js- class, and the component built on its elements. 18 * @param {{ajaxurl: string, nonce: string }} settings23 * @param {{ajaxurl: string, nonce: string, thousands: string}} settings 19 24 */ 20 25 constructor(el, components, settings) { 21 26 super(); 27 Screen.#thousands = settings.thousands ?? ','; 22 28 this.el = el; 23 29 this.settings = settings; … … 26 32 this.components = components.flatMap(({name, Class}) => [...el.querySelectorAll(`.${name}`)].map((element) => new Class(element, this))); 27 33 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; 28 48 } 29 49 -
takopic/trunk/core/Admin/Ajax.php
r3791227 r3791597 40 40 * Gives a script of a screen what its requests need 41 41 * (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. 43 44 * 44 45 * @param string $handle … … 50 51 public static function localize($handle, $name, $nonce, $data = []) 51 52 { 53 global $wp_locale; 52 54 wp_localize_script($handle, $name, array_merge([ 53 55 'ajaxurl' => admin_url('admin-ajax.php'), 54 56 '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'), 55 59 ], $data)); 56 60 } -
takopic/trunk/core/Cli/Commands.php
r3791227 r3791597 160 160 $counter = 0; 161 161 $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)); 164 169 $done = Lock::run($id, function () use ($id, $regenerate, $settings, $force, $thumbnails_only) { 165 170 $metadata = $regenerate ? Optimiser::regenerate($id) : null; … … 380 385 private function print_welcome() 381 386 { 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)); 383 389 } 384 390 -
takopic/trunk/core/Media/Optimiser.php
r3791227 r3791597 381 381 $metadata = wp_get_attachment_metadata($id); 382 382 $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())) { 384 384 $metadata = self::regenerate($id); 385 385 if ($metadata) { -
takopic/trunk/core/Media/Status.php
r3791227 r3791597 65 65 66 66 /** 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 /** 67 89 * 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. 70 93 * 71 94 * @return string … … 74 97 public static function sizes_signature() 75 98 { 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')])); 77 100 } 78 101 79 102 /** 80 103 * 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. 86 110 * 87 111 * @param array $metadata 112 * @param int $id 88 113 * @return bool 89 114 * @since 1.0.0 90 115 */ 91 public static function sizes_current($metadata )116 public static function sizes_current($metadata, $id = 0) 92 117 { 93 118 if (empty($metadata['width']) || empty($metadata['height'])) { … … 95 120 } 96 121 $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) { 98 123 $expected = image_resize_dimensions($metadata['width'], $metadata['height'], $size['width'], $size['height'], $size['crop']); 99 124 if (!$expected) { … … 117 142 public static function stamp_sizes($id, $metadata) 118 143 { 119 if (is_array($metadata) && self::sizes_current($metadata )) {144 if (is_array($metadata) && self::sizes_current($metadata, $id)) { 120 145 update_post_meta($id, self::SIZES_KEY, self::sizes_signature()); 121 146 } else { … … 287 312 'orderby' => 'ID', 288 313 '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, 289 317 ], $args); 290 318 if ($status === null) { -
takopic/trunk/readme.txt
r3791227 r3791597 6 6 Tested up to: 7.1 7 7 Requires PHP: 8.2 8 Stable tag: 1.0. 08 Stable tag: 1.0.1 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 119 119 == Changelog == 120 120 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`. 132 127 133 128 The 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 8 8 * Author URI: https://github.com/Striffly 9 9 * Text Domain: takopic 10 * Version: 1.0. 010 * Version: 1.0.1 11 11 * Requires at least: 6.2 12 12 * Requires PHP: 8.2 … … 23 23 24 24 // The version of Takopic, set by bun run release with the header above. 25 define('TAKOPIC_VERSION', '1.0. 0');25 define('TAKOPIC_VERSION', '1.0.1'); 26 26 define('TAKOPIC_FILE', __FILE__); 27 27 -
takopic/trunk/vendor-prefixed/autoload.php
r3791227 r3791597 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05::getLoader();22 return ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5::getLoader(); -
takopic/trunk/vendor-prefixed/composer/autoload_real.php
r3791227 r3791597 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba055 class ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Takopic\Vendor\Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 0933e086ea6d51c1829a5766a6c1ba05', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit52f6cf47167d7d979bd8eabd04daa9f5', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba05::getInitializer($loader));32 call_user_func(\Takopic\Vendor\Composer\Autoload\ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::getInitializer($loader)); 33 33 34 34 $loader->setClassMapAuthoritative(true); -
takopic/trunk/vendor-prefixed/composer/autoload_static.php
r3791227 r3791597 5 5 namespace Takopic\Vendor\Composer\Autoload; 6 6 7 class ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba057 class ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5 8 8 { 9 9 public static $classMap = array ( … … 16 16 { 17 17 return \Closure::bind(function () use ($loader) { 18 $loader->classMap = ComposerStaticInit 0933e086ea6d51c1829a5766a6c1ba05::$classMap;18 $loader->classMap = ComposerStaticInit52f6cf47167d7d979bd8eabd04daa9f5::$classMap; 19 19 20 20 }, null, ClassLoader::class); -
takopic/trunk/vendor-prefixed/composer/installed.php
r3791227 r3791597 3 3 array ( 4 4 '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', 8 8 'type' => 'wordpress-plugin', 9 9 'install_path' => __DIR__ . '/../',
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)