Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
651972d220 |
25
.htaccess
@ -2,17 +2,18 @@ DirectoryIndex index.php index.html
|
||||
Options -Indexes
|
||||
Options -MultiViews
|
||||
|
||||
# BEGIN WordPress
|
||||
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
|
||||
# dynamically generated, and should only be modified via WordPress filters.
|
||||
# Any changes to the directives between these markers will be overwritten.
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# 0) Serve existing files/directories as-is
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# 1) Internal map: /page or /page/ -> /page.php (if such PHP file exists)
|
||||
RewriteCond %{REQUEST_FILENAME}.php -f
|
||||
RewriteRule ^(.+?)/?$ $1.php [L]
|
||||
|
||||
# 2) Optional: strip trailing slash for non-directories (keeps .php links working)
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
RewriteBase /
|
||||
RewriteRule ^index\.php$ - [L]
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteRule ^(.+)/$ $1 [R=301,L]
|
||||
RewriteRule . /index.php [L]
|
||||
</IfModule>
|
||||
|
||||
# END WordPress
|
||||
@ -1,4 +1,4 @@
|
||||
WordPress Admin Credentials:
|
||||
URL: http://localhost/wp-admin
|
||||
Username: admin
|
||||
Password: c59u3v2geHuIQMRP
|
||||
Password: DA3st8vOerHPJqyhHy
|
||||
|
||||
1
wp-content/plugins/smart-image-resizer/assets/admin.css
Normal file
@ -0,0 +1 @@
|
||||
.sir-wrap{--sir-primary:#2563eb;--sir-ink:#0f172a;--sir-muted:#64748b;--sir-surface:#fff;--sir-soft:#eff6ff}.sir-hero{display:flex;justify-content:space-between;gap:28px;align-items:center;margin:22px 0;padding:32px;border-radius:24px;background:linear-gradient(135deg,#0f172a,#1d4ed8 58%,#38bdf8);color:#fff;box-shadow:0 22px 55px rgba(15,23,42,.18)}.sir-hero h1{font-size:42px;line-height:1;margin:0 0 12px}.sir-hero p{font-size:16px;max-width:760px;margin:0;color:#dbeafe}.sir-kicker{letter-spacing:.14em;text-transform:uppercase;font-size:12px!important;font-weight:700;color:#bfdbfe!important;margin-bottom:10px!important}.sir-stats{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.24);border-radius:20px;padding:22px 28px;text-align:center;min-width:140px}.sir-stats strong{display:block;font-size:42px;line-height:1}.sir-stats span{color:#e0f2fe}.sir-grid{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(300px,.8fr);gap:20px}.sir-card{background:#fff;border:1px solid #dbe3ef;border-radius:18px;padding:24px;box-shadow:0 10px 30px rgba(15,23,42,.06)}.sir-card-wide{grid-row:span 2}.sir-card h2{margin-top:0;color:var(--sir-ink);font-size:22px}.sir-table input[type=text],.sir-table input[type=number],.sir-table input:not([type]){width:100%;max-width:180px}.sir-toggle{display:block;margin:14px 0;font-weight:600}.sir-safe{background:#ecfdf5;border:1px solid #bbf7d0;color:#166534;border-radius:12px;padding:12px 14px}.sir-card select,.sir-card input[type=number]{width:100%;margin:8px 0 16px}.sir-log{margin:0}.sir-log li{padding:12px 0;border-bottom:1px solid #eef2f7}.sir-log strong{display:block;color:#0f172a}.sir-log span{color:#64748b}@media(max-width:960px){.sir-grid,.sir-hero{display:block}.sir-stats{margin-top:20px}.sir-hero h1{font-size:34px}}
|
||||
337
wp-content/plugins/smart-image-resizer/smart-image-resizer.php
Normal file
@ -0,0 +1,337 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Smart Image Resizer
|
||||
* Description: Resize Media Library images into admin-defined presets, bulk-generate copies, and optionally create WebP versions while keeping originals safe by default.
|
||||
* Version: 0.1.0
|
||||
* Author: Flatlogic AI
|
||||
* Requires at least: 6.0
|
||||
* Requires PHP: 8.0
|
||||
* Text Domain: smart-image-resizer
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
final class Smart_Image_Resizer {
|
||||
const OPTION = 'sir_settings';
|
||||
const LOG_OPTION = 'sir_recent_jobs';
|
||||
const META_KEY = '_sir_resized_files';
|
||||
const NONCE_ACTION = 'sir_admin_action';
|
||||
private static $instance = null;
|
||||
|
||||
public static function instance(): self {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function __construct() {
|
||||
add_action('admin_menu', [$this, 'admin_menu']);
|
||||
add_action('admin_init', [$this, 'handle_admin_actions']);
|
||||
add_action('admin_enqueue_scripts', [$this, 'admin_assets']);
|
||||
add_action('add_attachment', [$this, 'resize_on_upload']);
|
||||
register_activation_hook(__FILE__, [$this, 'activate']);
|
||||
}
|
||||
|
||||
public function activate(): void {
|
||||
if (!get_option(self::OPTION)) {
|
||||
add_option(self::OPTION, $this->default_settings());
|
||||
}
|
||||
}
|
||||
|
||||
public function default_settings(): array {
|
||||
return [
|
||||
'auto_resize' => 1,
|
||||
'webp' => 0,
|
||||
'keep_original' => 1,
|
||||
'presets' => [
|
||||
['name' => 'Card Thumbnail', 'slug' => 'card-thumb', 'width' => 480, 'height' => 320, 'crop' => 1],
|
||||
['name' => 'Content Medium', 'slug' => 'content-medium', 'width' => 960, 'height' => 0, 'crop' => 0],
|
||||
['name' => 'Hero Banner', 'slug' => 'hero-banner', 'width' => 1600, 'height' => 900, 'crop' => 1],
|
||||
['name' => 'Square Social', 'slug' => 'square-social', 'width' => 1080, 'height' => 1080, 'crop' => 1],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function get_settings(): array {
|
||||
$settings = get_option(self::OPTION, []);
|
||||
return wp_parse_args(is_array($settings) ? $settings : [], $this->default_settings());
|
||||
}
|
||||
|
||||
public function admin_menu(): void {
|
||||
add_media_page(
|
||||
__('Smart Image Resizer', 'smart-image-resizer'),
|
||||
__('Image Resizer', 'smart-image-resizer'),
|
||||
'upload_files',
|
||||
'smart-image-resizer',
|
||||
[$this, 'render_admin_page']
|
||||
);
|
||||
}
|
||||
|
||||
public function admin_assets(string $hook): void {
|
||||
if ($hook !== 'media_page_smart-image-resizer') {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style('sir-admin', plugin_dir_url(__FILE__) . 'assets/admin.css', [], '0.1.0');
|
||||
}
|
||||
|
||||
public function handle_admin_actions(): void {
|
||||
if (!is_admin() || empty($_POST['sir_action'])) {
|
||||
return;
|
||||
}
|
||||
if (!current_user_can('upload_files')) {
|
||||
wp_die(esc_html__('You do not have permission to resize images.', 'smart-image-resizer'));
|
||||
}
|
||||
check_admin_referer(self::NONCE_ACTION);
|
||||
$action = sanitize_key($_POST['sir_action']);
|
||||
|
||||
if ($action === 'save_settings') {
|
||||
$this->save_settings_from_post();
|
||||
wp_safe_redirect(add_query_arg(['page' => 'smart-image-resizer', 'sir_notice' => 'settings_saved'], admin_url('upload.php')));
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'bulk_resize') {
|
||||
$preset_slug = sanitize_title(wp_unslash($_POST['preset_slug'] ?? ''));
|
||||
$limit = max(1, min(50, absint($_POST['limit'] ?? 20)));
|
||||
$result = $this->bulk_resize($preset_slug, $limit);
|
||||
set_transient('sir_last_bulk_result_' . get_current_user_id(), $result, MINUTE_IN_SECONDS * 5);
|
||||
wp_safe_redirect(add_query_arg(['page' => 'smart-image-resizer', 'sir_notice' => 'bulk_done'], admin_url('upload.php')));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function save_settings_from_post(): void {
|
||||
$presets = [];
|
||||
$names = array_map('sanitize_text_field', wp_unslash($_POST['preset_name'] ?? []));
|
||||
$slugs = array_map('sanitize_title', wp_unslash($_POST['preset_slug'] ?? []));
|
||||
$widths = array_map('absint', wp_unslash($_POST['preset_width'] ?? []));
|
||||
$heights = array_map('absint', wp_unslash($_POST['preset_height'] ?? []));
|
||||
$crops = isset($_POST['preset_crop']) ? array_map('absint', wp_unslash($_POST['preset_crop'])) : [];
|
||||
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$name = trim($names[$i] ?? '');
|
||||
$slug = sanitize_title($slugs[$i] ?? $name);
|
||||
$width = $widths[$i] ?? 0;
|
||||
$height = $heights[$i] ?? 0;
|
||||
if (!$name || !$slug || $width < 1) {
|
||||
continue;
|
||||
}
|
||||
$presets[] = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'crop' => !empty($crops[$i]) ? 1 : 0,
|
||||
];
|
||||
}
|
||||
if (!$presets) {
|
||||
$presets = $this->default_settings()['presets'];
|
||||
}
|
||||
|
||||
update_option(self::OPTION, [
|
||||
'auto_resize' => !empty($_POST['auto_resize']) ? 1 : 0,
|
||||
'webp' => !empty($_POST['webp']) ? 1 : 0,
|
||||
'keep_original' => 1,
|
||||
'presets' => $presets,
|
||||
]);
|
||||
}
|
||||
|
||||
public function render_admin_page(): void {
|
||||
$settings = $this->get_settings();
|
||||
$presets = array_values($settings['presets']);
|
||||
$notice = sanitize_key($_GET['sir_notice'] ?? '');
|
||||
$last = get_transient('sir_last_bulk_result_' . get_current_user_id());
|
||||
?>
|
||||
<div class="wrap sir-wrap">
|
||||
<div class="sir-hero">
|
||||
<div>
|
||||
<p class="sir-kicker"><?php esc_html_e('Media performance toolkit', 'smart-image-resizer'); ?></p>
|
||||
<h1><?php esc_html_e('Smart Image Resizer', 'smart-image-resizer'); ?></h1>
|
||||
<p><?php esc_html_e('Generate consistent image sizes for your theme without touching originals. Resize new uploads automatically or process existing Media Library images in safe batches.', 'smart-image-resizer'); ?></p>
|
||||
</div>
|
||||
<div class="sir-stats">
|
||||
<strong><?php echo esc_html(count($presets)); ?></strong>
|
||||
<span><?php esc_html_e('active presets', 'smart-image-resizer'); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($notice === 'settings_saved') : ?>
|
||||
<div class="notice notice-success is-dismissible"><p><?php esc_html_e('Settings saved. Future uploads will use your latest presets.', 'smart-image-resizer'); ?></p></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($notice === 'bulk_done' && is_array($last)) : ?>
|
||||
<div class="notice notice-success is-dismissible"><p><?php printf(esc_html__('Bulk job complete: %1$d image(s) resized, %2$d skipped, %3$d error(s).', 'smart-image-resizer'), absint($last['resized']), absint($last['skipped']), absint($last['errors'])); ?></p></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="sir-grid">
|
||||
<form method="post" class="sir-card sir-card-wide">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="sir_action" value="save_settings">
|
||||
<h2><?php esc_html_e('Resize presets', 'smart-image-resizer'); ?></h2>
|
||||
<p class="description"><?php esc_html_e('Set up to six presets. Height can be 0 for proportional resizing. Crop creates exact dimensions.', 'smart-image-resizer'); ?></p>
|
||||
<table class="widefat striped sir-table">
|
||||
<thead><tr><th>Name</th><th>Slug</th><th>Width</th><th>Height</th><th>Crop</th></tr></thead>
|
||||
<tbody>
|
||||
<?php for ($i = 0; $i < 6; $i++) : $preset = $presets[$i] ?? ['name'=>'','slug'=>'','width'=>'','height'=>'','crop'=>0]; ?>
|
||||
<tr>
|
||||
<td><input class="regular-text" name="preset_name[]" value="<?php echo esc_attr($preset['name']); ?>" placeholder="Hero Banner"></td>
|
||||
<td><input name="preset_slug[]" value="<?php echo esc_attr($preset['slug']); ?>" placeholder="hero-banner"></td>
|
||||
<td><input type="number" min="0" name="preset_width[]" value="<?php echo esc_attr($preset['width']); ?>"></td>
|
||||
<td><input type="number" min="0" name="preset_height[]" value="<?php echo esc_attr($preset['height']); ?>"></td>
|
||||
<td><label><input type="checkbox" name="preset_crop[<?php echo esc_attr($i); ?>]" value="1" <?php checked(!empty($preset['crop'])); ?>> Exact crop</label></td>
|
||||
</tr>
|
||||
<?php endfor; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2><?php esc_html_e('Automation', 'smart-image-resizer'); ?></h2>
|
||||
<label class="sir-toggle"><input type="checkbox" name="auto_resize" value="1" <?php checked(!empty($settings['auto_resize'])); ?>> <?php esc_html_e('Resize new image uploads automatically', 'smart-image-resizer'); ?></label>
|
||||
<label class="sir-toggle"><input type="checkbox" name="webp" value="1" <?php checked(!empty($settings['webp'])); ?>> <?php esc_html_e('Also attempt WebP copies when the server supports it', 'smart-image-resizer'); ?></label>
|
||||
<p class="sir-safe"><?php esc_html_e('Safety mode is on: originals are kept by default for reversible workflows.', 'smart-image-resizer'); ?></p>
|
||||
<?php submit_button(__('Save settings', 'smart-image-resizer')); ?>
|
||||
</form>
|
||||
|
||||
<div class="sir-card">
|
||||
<h2><?php esc_html_e('Bulk resize existing media', 'smart-image-resizer'); ?></h2>
|
||||
<p><?php esc_html_e('Process a small batch of existing images. Re-run batches until your library is complete.', 'smart-image-resizer'); ?></p>
|
||||
<form method="post">
|
||||
<?php wp_nonce_field(self::NONCE_ACTION); ?>
|
||||
<input type="hidden" name="sir_action" value="bulk_resize">
|
||||
<label for="sir-preset"><strong><?php esc_html_e('Preset', 'smart-image-resizer'); ?></strong></label>
|
||||
<select id="sir-preset" name="preset_slug">
|
||||
<?php foreach ($presets as $preset) : ?>
|
||||
<option value="<?php echo esc_attr($preset['slug']); ?>"><?php echo esc_html($preset['name']); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<label for="sir-limit"><strong><?php esc_html_e('Batch size', 'smart-image-resizer'); ?></strong></label>
|
||||
<input id="sir-limit" type="number" name="limit" value="20" min="1" max="50">
|
||||
<?php submit_button(__('Run bulk resize', 'smart-image-resizer'), 'primary', 'submit', false); ?>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sir-card">
|
||||
<h2><?php esc_html_e('Recent activity', 'smart-image-resizer'); ?></h2>
|
||||
<?php $this->render_logs(); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
private function render_logs(): void {
|
||||
$logs = get_option(self::LOG_OPTION, []);
|
||||
if (!$logs) {
|
||||
echo '<p class="description">' . esc_html__('No resize jobs yet. Upload an image or run a bulk job to see activity here.', 'smart-image-resizer') . '</p>';
|
||||
return;
|
||||
}
|
||||
echo '<ul class="sir-log">';
|
||||
foreach (array_slice(array_reverse($logs), 0, 8) as $log) {
|
||||
printf('<li><strong>%s</strong><span>%s</span></li>', esc_html($log['title'] ?? 'Image'), esc_html($log['message'] ?? 'Resized'));
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
public function resize_on_upload(int $attachment_id): void {
|
||||
$settings = $this->get_settings();
|
||||
if (empty($settings['auto_resize']) || !wp_attachment_is_image($attachment_id)) {
|
||||
return;
|
||||
}
|
||||
foreach ($settings['presets'] as $preset) {
|
||||
$this->resize_attachment($attachment_id, $preset, !empty($settings['webp']));
|
||||
}
|
||||
}
|
||||
|
||||
public function bulk_resize(string $preset_slug, int $limit): array {
|
||||
$settings = $this->get_settings();
|
||||
$preset = null;
|
||||
foreach ($settings['presets'] as $candidate) {
|
||||
if ($candidate['slug'] === $preset_slug) {
|
||||
$preset = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$preset) {
|
||||
return ['resized' => 0, 'skipped' => 0, 'errors' => 1];
|
||||
}
|
||||
$ids = get_posts([
|
||||
'post_type' => 'attachment',
|
||||
'post_mime_type' => 'image',
|
||||
'post_status' => 'inherit',
|
||||
'posts_per_page' => $limit,
|
||||
'fields' => 'ids',
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
]);
|
||||
$result = ['resized' => 0, 'skipped' => 0, 'errors' => 0];
|
||||
foreach ($ids as $id) {
|
||||
$status = $this->resize_attachment((int) $id, $preset, !empty($settings['webp']));
|
||||
if ($status === true) {
|
||||
$result['resized']++;
|
||||
} elseif ($status === 'exists') {
|
||||
$result['skipped']++;
|
||||
} else {
|
||||
$result['errors']++;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function resize_attachment(int $attachment_id, array $preset, bool $make_webp = false) {
|
||||
$file = get_attached_file($attachment_id);
|
||||
if (!$file || !file_exists($file)) {
|
||||
return false;
|
||||
}
|
||||
$editor = wp_get_image_editor($file);
|
||||
if (is_wp_error($editor)) {
|
||||
return false;
|
||||
}
|
||||
$info = pathinfo($file);
|
||||
$slug = sanitize_title($preset['slug']);
|
||||
$width = absint($preset['width']);
|
||||
$height = absint($preset['height']);
|
||||
$dest = trailingslashit($info['dirname']) . $info['filename'] . '-' . $slug . '-' . $width . 'x' . ($height ?: 'auto') . '.' . strtolower($info['extension']);
|
||||
$existing = get_post_meta($attachment_id, self::META_KEY, true);
|
||||
$existing = is_array($existing) ? $existing : [];
|
||||
if (file_exists($dest)) {
|
||||
return 'exists';
|
||||
}
|
||||
$resized = $editor->resize($width, $height ?: null, !empty($preset['crop']));
|
||||
if (is_wp_error($resized)) {
|
||||
return false;
|
||||
}
|
||||
$saved = $editor->save($dest);
|
||||
if (is_wp_error($saved) || empty($saved['path'])) {
|
||||
return false;
|
||||
}
|
||||
$existing[$slug] = [
|
||||
'path' => $saved['path'],
|
||||
'file' => wp_basename($saved['path']),
|
||||
'width' => $saved['width'] ?? $width,
|
||||
'height' => $saved['height'] ?? $height,
|
||||
'created' => current_time('mysql'),
|
||||
];
|
||||
if ($make_webp && function_exists('imagewebp')) {
|
||||
$webp_editor = wp_get_image_editor($saved['path']);
|
||||
if (!is_wp_error($webp_editor)) {
|
||||
$webp_dest = preg_replace('/\.[^.]+$/', '.webp', $saved['path']);
|
||||
$webp_saved = $webp_editor->save($webp_dest, 'image/webp');
|
||||
if (!is_wp_error($webp_saved) && !empty($webp_saved['path'])) {
|
||||
$existing[$slug]['webp'] = wp_basename($webp_saved['path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
update_post_meta($attachment_id, self::META_KEY, $existing);
|
||||
$this->add_log(get_the_title($attachment_id), sprintf('Created %s at %sx%s', $preset['name'], $existing[$slug]['width'], $existing[$slug]['height']));
|
||||
return true;
|
||||
}
|
||||
|
||||
private function add_log(string $title, string $message): void {
|
||||
$logs = get_option(self::LOG_OPTION, []);
|
||||
$logs[] = ['time' => current_time('mysql'), 'title' => $title ?: 'Untitled image', 'message' => $message];
|
||||
update_option(self::LOG_OPTION, array_slice($logs, -30));
|
||||
}
|
||||
}
|
||||
|
||||
Smart_Image_Resizer::instance();
|
||||
BIN
wp-content/uploads/2026/06/sir-test-source-1024x683.jpg
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
wp-content/uploads/2026/06/sir-test-source-150x150.jpg
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
wp-content/uploads/2026/06/sir-test-source-300x200.jpg
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
wp-content/uploads/2026/06/sir-test-source-768x512.jpg
Normal file
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
wp-content/uploads/2026/06/sir-test-source.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
wp-content/uploads/sir-test-source.jpg
Normal file
|
After Width: | Height: | Size: 31 KiB |