Compare commits

...

1 Commits

Author SHA1 Message Date
Flatlogic Bot
651972d220 1 2026-06-04 12:34:34 +00:00
14 changed files with 352 additions and 13 deletions

View File

@ -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

View File

@ -1,4 +1,4 @@
WordPress Admin Credentials:
URL: http://localhost/wp-admin
Username: admin
Password: c59u3v2geHuIQMRP
Password: DA3st8vOerHPJqyhHy

View 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}}

View 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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.