Home / Admin / Cortex Image Meta API
Duplicate Snippet

Embed Snippet on Your Site

Cortex Image Meta API

<10
Code Preview
php
<?php
/**
 * Cortex Image Meta API - REST Endpoints for WordPress Image Optimization
 * v1.0 - Separated from Cortex SEO Meta API
 * 
 * For image alt text, titles, captions, and optimization scoring.
 * Works with RankMath focus keywords for context-aware optimization.
 *
 * ============================================================================
 * IMAGE SEO BEST PRACTICES 2025
 * ============================================================================
 *
 * ALT TEXT:
 *   - 25-125 characters, descriptive
 *   - Format: [Action/Description] + [Subject] + [Context/Location]
 *   - Include parent page focus keyword naturally
 *   - Describe what you SEE, not what you assume
 *
 * TITLE:
 *   - Shorter version of alt, max 60 characters
 *   - Not generic (avoid "image1", "photo", "IMG_xxxx")
 *
 * FILENAME:
 *   - Keyword-rich, descriptive (not IMG_xxxx)
 *   - Cannot be changed via API (would break URLs)
 *
 * FORMAT & SIZE:
 *   - WebP preferred for compression
 *   - <200KB standard images, <500KB hero images
 *   - Max 2000px dimensions for web
 *
 * ============================================================================
 */
// ============================================================================
// CONFIGURATION
// ============================================================================
define('CORTEX_IMAGE_CACHE_TTL', HOUR_IN_SECONDS);
define('CORTEX_IMAGE_BULK_LIMIT', 50);
define('CORTEX_IMAGE_PREVIEW_LIMIT', 25);
define('CORTEX_AUDIT_SAMPLE_LIMIT', 50);
define('CORTEX_LICENSE_PAGE', '/image-licensing/');
// ============================================================================
// REST API REGISTRATION
// ============================================================================
add_action('rest_api_init', function() {
    
    // GET /wp-json/cortex/v1/images - List images with metadata
    register_rest_route('cortex/v1', '/images', array(
        'methods' => 'GET',
        'callback' => 'cortex_get_images',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // GET /wp-json/cortex/v1/images/{id} - Get single image details
    register_rest_route('cortex/v1', '/images/(?P<id>\d+)', array(
        'methods' => 'GET',
        'callback' => 'cortex_get_image',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // POST /wp-json/cortex/v1/images/{id} - Update single image
    register_rest_route('cortex/v1', '/images/(?P<id>\d+)', array(
        'methods' => 'POST',
        'callback' => 'cortex_update_image',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // POST /wp-json/cortex/v1/images/bulk - Bulk update images
    register_rest_route('cortex/v1', '/images/bulk', array(
        'methods' => 'POST',
        'callback' => 'cortex_bulk_update_images',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // GET /wp-json/cortex/v1/images/audit - Audit summary
    register_rest_route('cortex/v1', '/images/audit', array(
        'methods' => 'GET',
        'callback' => 'cortex_audit_images',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // POST /wp-json/cortex/v1/images/bulk-preview - Get thumbnails + metadata
    register_rest_route('cortex/v1', '/images/bulk-preview', array(
        'methods' => 'POST',
        'callback' => 'cortex_bulk_preview_images',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // POST /wp-json/cortex/v1/images/bulk-get - Get metadata only
    register_rest_route('cortex/v1', '/images/bulk-get', array(
        'methods' => 'POST',
        'callback' => 'cortex_bulk_get_images',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // GET /wp-json/cortex/v1/images/audit-url - Audit by URL (direct API)
    register_rest_route('cortex/v1', '/images/audit-url', array(
        'methods' => 'GET',
        'callback' => 'cortex_audit_images_by_url',
        'permission_callback' => 'cortex_image_permission_check',
    ));
    
    // POST /wp-json/cortex/v1/images/audit-url - Audit by URL (MCP compatible)
    register_rest_route('cortex/v1', '/images/audit-url', array(
        'methods' => 'POST',
        'callback' => 'cortex_audit_images_by_url_post',
        'permission_callback' => 'cortex_image_permission_check',
        'args' => array(
            'url' => array(
                'required' => true,
                'type' => 'string',
                'description' => 'The page URL to audit images for',
                'sanitize_callback' => 'esc_url_raw',
            ),
            'include_preview' => array(
                'required' => false,
                'type' => 'boolean',
                'default' => false,
            ),
            'preview_size' => array(
                'required' => false,
                'type' => 'string',
                'default' => 'thumbnail',
                'enum' => array('thumbnail', 'medium', 'large'),
            ),
        ),
    ));
});
// ============================================================================
// PERMISSION CHECK
// ============================================================================
function cortex_image_permission_check() {
    return current_user_can('edit_posts');
}
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
function cortex_image_cache_key($attachment_id) {
    return 'cortex_image_meta_' . $attachment_id;
}
// Clear image cache on update
if (is_admin()) {
    add_action('edit_attachment', function($attachment_id) {
        wp_cache_delete(cortex_image_cache_key($attachment_id));
    });
}
/**
 * Get IDs of all images currently IN USE on published content
 */
function cortex_get_in_use_image_ids() {
    global $wpdb;
    
    $cache_key = 'cortex_in_use_image_ids';
    $cached = wp_cache_get($cache_key);
    if ($cached !== false) {
        return $cached;
    }
    
    $all_ids = array();
    
    // Featured images
    $featured_ids = $wpdb->get_col("
        SELECT DISTINCT pm.meta_value 
        FROM {$wpdb->postmeta} pm
        INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
        WHERE pm.meta_key = '_thumbnail_id' 
        AND p.post_status = 'publish'
        AND pm.meta_value > 0
    ");
    $all_ids = array_merge($all_ids, array_map('intval', $featured_ids));
    
    // Images in content (wp-image-{id})
    $published_posts = $wpdb->get_results("
        SELECT ID, post_content 
        FROM {$wpdb->posts} 
        WHERE post_status = 'publish' 
        AND post_content LIKE '%wp-image-%'
    ");
    
    foreach ($published_posts as $post) {
        if (preg_match_all('/wp-image-(\d+)/', $post->post_content, $matches)) {
            $all_ids = array_merge($all_ids, array_map('intval', $matches[1]));
        }
    }
    
    // Images with published parent
    $parent_ids = $wpdb->get_col("
        SELECT a.ID
        FROM {$wpdb->posts} a
        INNER JOIN {$wpdb->posts} p ON a.post_parent = p.ID
        WHERE a.post_type = 'attachment'
        AND a.post_mime_type LIKE 'image/%'
        AND p.post_status = 'publish'
    ");
    $all_ids = array_merge($all_ids, array_map('intval', $parent_ids));
    
    $unique_ids = array_unique(array_filter($all_ids));
    
    if (!empty($unique_ids)) {
        $ids_string = implode(',', $unique_ids);
        $verified_ids = $wpdb->get_col("
            SELECT ID FROM {$wpdb->posts}
            WHERE ID IN ({$ids_string})
            AND post_type = 'attachment'
            AND post_mime_type LIKE 'image/%'
        ");
        $unique_ids = array_map('intval', $verified_ids);
    }
    
    wp_cache_set($cache_key, $unique_ids, '', 300);
    
    return $unique_ids;
}
/**
 * Classify image type based on filename, dimensions, and context
 */
function cortex_classify_image($attachment_id, $metadata = null) {
    $filename = basename(get_attached_file($attachment_id));
    $filename_lower = strtolower($filename);
    
    $ui_patterns = array('arrow', 'divider', 'icon', 'spacer', 'bullet', 'separator', 'border', 'shadow', 'bg-', 'background-');
    foreach ($ui_patterns as $pattern) {
        if (strpos($filename_lower, $pattern) !== false) return 'ui';
    }
    
    if ($metadata === null) {
        $metadata = wp_get_attachment_metadata($attachment_id);
    }
    if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
        if ($metadata['width'] < 100 || $metadata['height'] < 100) return 'ui';
    }
    
    global $wpdb;
    $is_featured = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->postmeta} WHERE meta_key = '_thumbnail_id' AND meta_value = %d",
        $attachment_id
    ));
    if ($is_featured > 0) return 'content';
    
    $in_content = $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_content LIKE %s",
        '%wp-image-' . $attachment_id . '%'
    ));
    if ($in_content > 0) return 'content';
    
    return 'decorative';
}
/**
 * Calculate optimization score for an image
 */
function cortex_calculate_image_score($attachment_id, $alt_text, $title, $caption, $metadata, $parent_focus_keyword = '') {
    $score = 0;
    $issues = array();
    $filename = basename(get_attached_file($attachment_id));
    $mime_type = get_post_mime_type($attachment_id);
    
    // Alt text present (15 pts)
    if (!empty($alt_text)) {
        $score += 15;
        $alt_len = strlen($alt_text);
        if ($alt_len >= 25 && $alt_len <= 125) {
            $score += 10;
        } else {
            $issues[] = $alt_len < 25 ? 'alt_too_short' : 'alt_too_long';
        }
        
        if (!empty($parent_focus_keyword)) {
            $keywords = array_map('trim', explode(',', $parent_focus_keyword));
            $keyword_found = false;
            foreach ($keywords as $keyword) {
                if (!empty($keyword) && stripos($alt_text, $keyword) !== false) {
                    $keyword_found = true;
                    break;
                }
            }
            if ($keyword_found) {
                $score += 10;
            } else {
                $issues[] = 'alt_missing_keyword';
            }
        }
    } else {
        $issues[] = 'missing_alt';
    }
    
    // Title (10 pts)
    if (!empty($title)) {
        $score += 10;
        if (!preg_match('/^(image|img|photo|pic|picture|untitled|dsc|dcim|\d+)/i', $title) && strlen($title) > 5) {
            $score += 5;
        } else {
            $issues[] = 'title_generic';
        }
    } else {
        $issues[] = 'missing_title';
    }
    
    // Filename (15 pts)
    if (!preg_match('/^(IMG_|DSC_|DCIM|Photo|Screenshot|Screen Shot|\d{4,})/i', $filename)) {
        $score += 15;
    } else {
        $issues[] = 'filename_generic';
    }
    
    // WebP (10 pts)
    if ($mime_type === 'image/webp') {
        $score += 10;
    } else {
        $issues[] = 'not_webp';
    }
    
    // Dimensions (10 pts)
    if ($metadata && isset($metadata['width']) && isset($metadata['height'])) {
        if ($metadata['width'] <= 2000 && $metadata['height'] <= 2000) {
            $score += 10;
        } else {
            $issues[] = 'oversized_dimensions';
        }
    }
    
    // File size (10 pts)
    $file_path = get_attached_file($attachment_id);
    if ($file_path && file_exists($file_path)) {
        $file_size = filesize($file_path);
        $is_hero = $metadata && isset($metadata['width']) && $metadata['width'] >= 1200;
        $size_limit = $is_hero ? 500 * 1024 : 200 * 1024;
        
        if ($file_size <= $size_limit) {
            $score += 10;
        } else {
            $issues[] = 'file_too_large';
        }
    }
    
    // Caption (5 pts bonus)
    if (!empty($caption)) {
        $score += 5;
    }
    
    return array(
        'score' => min($score, 100),
        'issues' => $issues
    );
}
/**
 * Get parent posts for an attachment
 */
function cortex_get_image_parent_posts($attachment_id) {
    global $wpdb;
    $parents = array();
    
    // Featured image relationships
    $featured_posts = $wpdb->get_results($wpdb->prepare(
        "SELECT p.ID, p.post_title, p.post_excerpt, p.post_type 
         FROM {$wpdb->posts} p 
         INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id 
         WHERE pm.meta_key = '_thumbnail_id' AND pm.meta_value = %d AND p.post_status = 'publish'",
        $attachment_id
    ));
    
    foreach ($featured_posts as $post) {
        $parents[] = array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'url' => get_permalink($post->ID),
            'focus_keyword' => get_post_meta($post->ID, 'rank_math_focus_keyword', true),
            'post_type' => $post->post_type,
            'relationship' => 'featured_image'
        );
    }
    
    // In-content relationships
    $content_posts = $wpdb->get_results($wpdb->prepare(
        "SELECT ID, post_title, post_excerpt, post_type 
         FROM {$wpdb->posts} 
         WHERE post_status = 'publish' AND post_content LIKE %s",
        '%wp-image-' . $attachment_id . '%'
    ));
    
    foreach ($content_posts as $post) {
        $exists = false;
        foreach ($parents as $p) {
            if ($p['id'] == $post->ID) { $exists = true; break; }
        }
        if ($exists) continue;
        
        $parents[] = array(
            'id' => $post->ID,
            'title' => $post->post_title,
            'url' => get_permalink($post->ID),
            'focus_keyword' => get_post_meta($post->ID, 'rank_math_focus_keyword', true),
            'post_type' => $post->post_type,
            'relationship' => 'in_content'
        );
    }
    
    // Upload parent
    $attachment = get_post($attachment_id);
    if ($attachment && $attachment->post_parent > 0) {
        $parent = get_post($attachment->post_parent);
        if ($parent && $parent->post_status === 'publish') {
            $exists = false;
            foreach ($parents as $p) {
                if ($p['id'] == $parent->ID) { $exists = true; break; }
            }
            if (!$exists) {
                $parents[] = array(
                    'id' => $parent->ID,
                    'title' => $parent->post_title,
                    'url' => get_permalink($parent->ID),
                    'focus_keyword' => get_post_meta($parent->ID, 'rank_math_focus_keyword', true),
                    'post_type' => $parent->post_type,
                    'relationship' => 'upload_parent'
                );
            }
        }
    }
    
    return $parents;
}
/**
 * Build complete image data array
 */
function cortex_build_image_data($attachment_id, $include_parents = true) {
    $cached = wp_cache_get(cortex_image_cache_key($attachment_id));
    if ($cached !== false && !$include_parents) return $cached;
    
    $attachment = get_post($attachment_id);
    if (!$attachment || $attachment->post_type !== 'attachment') return null;
    if (!wp_attachment_is_image($attachment_id)) return null;
    
    $metadata = wp_get_attachment_metadata($attachment_id);
    $alt_text = get_post_meta($attachment_id, '_wp_attachment_image_alt', true);
    $file_path = get_attached_file($attachment_id);
    
    $parent_posts = $include_parents ? cortex_get_image_parent_posts($attachment_id) : array();
    $primary_keyword = '';
    if (!empty($parent_posts) && !empty($parent_posts[0]['focus_keyword'])) {
        $primary_keyword = $parent_posts[0]['focus_keyword'];
    }
    
    $score_data = cortex_calculate_image_score(
        $attachment_id,
        $alt_text,
        $attachment->post_title,
        $attachment->post_excerpt,
        $metadata,
        $primary_keyword
    );
    
    $data = array(
        'id' => $attachment_id,
        'url' => wp_get_attachment_url($attachment_id),
        'filename' => basename($file_path),
        'alt_text' => $alt_text,
        'title' => $attachment->post_title,
        'caption' => $attachment->post_excerpt,
        'description' => $attachment->post_content,
        'dimensions' => array(
            'width' => $metadata['width'] ?? 0,
            'height' => $metadata['height'] ?? 0
        ),
        'file_size' => file_exists($file_path) ? filesize($file_path) : 0,
        'file_size_formatted' => file_exists($file_path) ? size_format(filesize($file_path)) : '0 B',
        'mime_type' => get_post_mime_type($attachment_id),
        'optimization_score' => $score_data['score'],
        'issues' => $score_data['issues'],
        'image_type' => cortex_classify_image($attachment_id, $metadata),
        'parent_posts' => $parent_posts,
        'date_uploaded' => $attachment->post_date,
        'date_modified' => $attachment->post_modified
    );
    
    if (!$include_parents) {
        wp_cache_set(cortex_image_cache_key($attachment_id), $data, '', CORTEX_IMAGE_CACHE_TTL);
    }
    
    return $data;
}
/**
 * Get base64-encoded image for a specific size
 */
function cortex_get_image_base64($attachment_id, $size = 'thumbnail') {
    $image_src = wp_get_attachment_image_src($attachment_id, $size);
    
    if (!$image_src) return null;
    
    $image_url = $image_src[0];
    $upload_dir = wp_upload_dir();
    $file_path = str_replace($upload_dir['baseurl'], $upload_dir['basedir'], $image_url);
    
    if (!file_exists($file_path)) {
        $file_path = get_attached_file($attachment_id);
    }
    
    if (!$file_path || !file_exists($file_path)) return null;
    
    $mime_type = get_post_mime_type($attachment_id);
    $image_data = file_get_contents($file_path);
    if ($image_data === false) return null;
    
    return 'data:' . $mime_type . ';base64,' . base64_encode($image_data);
}
// ============================================================================
// ENDPOINT CALLBACKS
// ============================================================================
function cortex_get_images($request) {
    $per_page = $request->get_param('per_page') ? min(intval($request->get_param('per_page')), 100) : 50;
    $page = $request->get_param('page') ? intval($request->get_param('page')) : 1;
    $in_use_only = $request->get_param('in_use_only');
    $min_score = $request->get_param('min_score');
    $max_score = $request->get_param('max_score');
    $image_type = $request->get_param('image_type');
    $has_issues = $request->get_param('has_issues');
    
    $filter_in_use = ($in_use_only !== 'false' && $in_use_only !== '0');
    
    if ($filter_in_use) {
        $in_use_ids = cortex_get_in_use_image_ids();
        
        if (empty($in_use_ids)) {
            return array(
                'items' => array(),
                'pagination' => array('total' => 0, 'per_page' => $per_page, 'current_page' => $page, 'total_pages' => 0),
                'count' => 0
            );
        }
        
        $total = count($in_use_ids);
        $offset = ($page - 1) * $per_page;
        $paged_ids = array_slice($in_use_ids, $offset, $per_page);
        
        $images = array();
        foreach ($paged_ids as $attachment_id) {
            $image_data = cortex_build_image_data($attachment_id);
            if (!$image_data) continue;
            
            if ($min_score !== null && $image_data['optimization_score'] < intval($min_score)) continue;
            if ($max_score !== null && $image_data['optimization_score'] > intval($max_score)) continue;
            if ($image_type && $image_data['image_type'] !== $image_type) continue;
            if ($has_issues === 'true' && empty($image_data['issues'])) continue;
            if ($has_issues === 'false' && !empty($image_data['issues'])) continue;
            
            $images[] = $image_data;
        }
        
        return array(
            'items' => $images,
            'pagination' => array('total' => $total, 'per_page' => $per_page, 'current_page' => $page, 'total_pages' => ceil($total / $per_page)),
            'count' => count($images),
            'filter' => 'in_use_only'
        );
    }
    
    $args = array(
        'post_type' => 'attachment',
        'post_mime_type' => 'image',
        'post_status' => 'inherit',
        'posts_per_page' => $per_page,
        'paged' => $page,
        'orderby' => 'date',
        'order' => 'DESC'
    );
    
    $query = new WP_Query($args);
    $images = array();
    
    foreach ($query->posts as $attachment) {
        $image_data = cortex_build_image_data($attachment->ID);
        if (!$image_data) continue;
        
        if ($min_score !== null && $image_data['optimization_score'] < intval($min_score)) continue;
        if ($max_score !== null && $image_data['optimization_score'] > intval($max_score)) continue;
        if ($image_type && $image_data['image_type'] !== $image_type) continue;
        if ($has_issues === 'true' && empty($image_data['issues'])) continue;
        if ($has_issues === 'false' && !empty($image_data['issues'])) continue;
        
        $images[] = $image_data;
    }
    
    return array(
        'items' => $images,
        'pagination' => array('total' => $query->found_posts, 'per_page' => $per_page, 'current_page' => $page, 'total_pages' => $query->max_num_pages),
        'count' => count($images),
        'filter' => 'all_images'
    );
}
function cortex_get_image($request) {
    $attachment_id = intval($request->get_param('id'));
    $image_data = cortex_build_image_data($attachment_id, true);
    
    if (!$image_data) {
        return new WP_Error('not_found', 'Image not found', array('status' => 404));
    }
    
    return $image_data;
}
function cortex_update_image($request) {
    $attachment_id = intval($request->get_param('id'));
    $attachment = get_post($attachment_id);
    
    if (!$attachment || $attachment->post_type !== 'attachment') {
        return new WP_Error('not_found', 'Image not found', array('status' => 404));
    }
    
    $updated = array();
    $post_updates = array('ID' => $attachment_id);
    
    $alt_text = $request->get_param('alt_text');
    if ($alt_text !== null) {
        update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($alt_text));
        $updated['alt_text'] = $alt_text;
    }
    
    $title = $request->get_param('title');
    if ($title !== null) {
        $post_updates['post_title'] = sanitize_text_field($title);
        $updated['title'] = $title;
    }
    
    $caption = $request->get_param('caption');
    if ($caption !== null) {
        $post_updates['post_excerpt'] = sanitize_textarea_field($caption);
        $updated['caption'] = $caption;
    }
    
    $description = $request->get_param('description');
    if ($description !== null) {
        $post_updates['post_content'] = sanitize_textarea_field($description);
        $updated['description'] = $description;
    }
    
    if (count($post_updates) > 1) {
        wp_update_post($post_updates);
    }
    
    if (empty($updated)) {
        return new WP_Error('no_fields', 'No valid fields provided to update', array('status' => 400));
    }
    
    wp_cache_delete(cortex_image_cache_key($attachment_id));
    
    return array(
        'success' => true,
        'id' => $attachment_id,
        'updated_fields' => $updated,
        'fields_count' => count($updated),
        'current_data' => cortex_build_image_data($attachment_id),
        'message' => count($updated) . ' field(s) updated successfully'
    );
}
function cortex_bulk_update_images($request) {
    $items = $request->get_param('items');
    
    if (!$items || !is_array($items)) {
        return new WP_Error('invalid_payload', 'Expected "items" array in request body', array('status' => 400));
    }
    
    if (count($items) > CORTEX_IMAGE_BULK_LIMIT) {
        return new WP_Error('too_many_items', 'Maximum ' . CORTEX_IMAGE_BULK_LIMIT . ' items per bulk request', array('status' => 400));
    }
    
    $results = array(
        'success' => array(),
        'failed' => array(),
        'summary' => array('total' => count($items), 'succeeded' => 0, 'failed' => 0, 'fields_updated' => 0)
    );
    
    foreach ($items as $item) {
        if (!isset($item['id'])) {
            $results['failed'][] = array('error' => 'Missing id field', 'item' => $item);
            $results['summary']['failed']++;
            continue;
        }
        
        $attachment_id = intval($item['id']);
        $attachment = get_post($attachment_id);
        
        if (!$attachment || $attachment->post_type !== 'attachment') {
            $results['failed'][] = array('id' => $attachment_id, 'error' => 'Image not found');
            $results['summary']['failed']++;
            continue;
        }
        
        $updated = array();
        $post_updates = array('ID' => $attachment_id);
        
        if (isset($item['alt_text'])) {
            update_post_meta($attachment_id, '_wp_attachment_image_alt', sanitize_text_field($item['alt_text']));
            $updated['alt_text'] = $item['alt_text'];
        }
        
        if (isset($item['title'])) {
            $post_updates['post_title'] = sanitize_text_field($item['title']);
            $updated['title'] = $item['title'];
        }
        
        if (isset($item['caption'])) {
            $post_updates['post_excerpt'] = sanitize_textarea_field($item['caption']);
            $updated['caption'] = $item['caption'];
        }
        
        if (isset($item['description'])) {
            $post_updates['post_content'] = sanitize_textarea_field($item['description']);
            $updated['description'] = $item['description'];
        }
        
        if (count($post_updates) > 1) {
            wp_update_post($post_updates);
        }
        
        wp_cache_delete(cortex_image_cache_key($attachment_id));
        
        $results['success'][] = array('id' => $attachment_id, 'updated_fields' => $updated, 'fields_count' => count($updated));
        $results['summary']['succeeded']++;
        $results['summary']['fields_updated'] += count($updated);
    }
    
    return $results;
}
function cortex_bulk_preview_images($request) {
    $ids = $request->get_param('ids');
    $size = $request->get_param('size') ?: 'thumbnail';
    
    if (!$ids || !is_array($ids)) {
        return new WP_Error('invalid_payload', 'Expected "ids" array in request body', array('status' => 400));
    }
    
    if (count($ids) > CORTEX_IMAGE_PREVIEW_LIMIT) {
        return new WP_Error('too_many_ids', 'Maximum ' . CORTEX_IMAGE_PREVIEW_LIMIT . ' images per bulk preview', array('status' => 400));
    }
    
    $valid_sizes = array('thumbnail', 'medium', 'large');
    if (!in_array($size, $valid_sizes)) $size = 'thumbnail';
    
    $images = array();
    $failed = array();
    
    foreach ($ids as $id) {
        $attachment_id = intval($id);
        $image_data = cortex_build_image_data($attachment_id, true);
        
        if (!$image_data) {
            $failed[] = array('id' => $attachment_id, 'error' => 'Image not found');
            continue;
        }
        
        $base64 = cortex_get_image_base64($attachment_id, $size);
        
        if (!$base64) {
            $failed[] = array('id' => $attachment_id, 'error' => 'Could not generate preview');
            continue;
        }
        
        $preview = array(
            'id' => $attachment_id,
            'preview' => $base64,
            'preview_size' => $size,
            'filename' => $image_data['filename'],
            'current_alt' => $image_data['alt_text'],
            'current_title' => $image_data['title'],
            'dimensions' => $image_data['dimensions'],
            'optimization_score' => $image_data['optimization_score'],
            'issues' => $image_data['issues'],
            'image_type' => $image_data['image_type'],
            'parent_focus_keyword' => '',
            'parent_post_title' => ''
        );
        
        if (!empty($image_data['parent_posts'])) {
            $primary_parent = $image_data['parent_posts'][0];
            $preview['parent_focus_keyword'] = $primary_parent['focus_keyword'] ?? '';
            $preview['parent_post_title'] = $primary_parent['title'] ?? '';
        }
        
        $images[] = $preview;
    }
    
    return array(
        'images' => $images,
        'failed' => $failed,
        'count' => count($images),
        'size' => $size,
        'summary' => array('requested' => count($ids), 'returned' => count($images), 'failed' => count($failed))
    );
}
function cortex_bulk_get_images($request) {
    $ids = $request->get_param('ids');
    
    if (!$ids || !is_array($ids)) {
        return new WP_Error('invalid_payload', 'Expected "ids" array in request body', array('status' => 400));
    }
    
    if (count($ids) > CORTEX_IMAGE_BULK_LIMIT) {
        return new WP_Error('too_many_ids', 'Maximum ' . CORTEX_IMAGE_BULK_LIMIT . ' images per request', array('status' => 400));
    }
    
    $images = array();
    $not_found = array();
    
    foreach ($ids as $id) {
        $attachment_id = intval($id);
        $image_data = cortex_build_image_data($attachment_id, true);
        
        if ($image_data) {
            $images[] = $image_data;
        } else {
            $not_found[] = $attachment_id;
        }
    }
    
    return array(
        'images' => $images,
        'not_found' => $not_found,
        'count' => count($images),
        'summary' => array('requested' => count($ids), 'returned' => count($images), 'not_found' => count($not_found))
    );
}
function cortex_audit_images_by_url_core($url, $include_preview = false, $preview_size = 'thumbnail') {
    if (empty($url)) {
        return new WP_Error('missing_url', 'URL parameter is required', array('status' => 400));
    }
    
    $url = esc_url_raw($url);
    $post_id = url_to_postid($url);
    
    if (!$post_id) {
        $url_without_trailing = rtrim($url, '/');
        $post_id = url_to_postid($url_without_trailing);
    }
    
    if (!$post_id) {
        $url_with_trailing = trailingslashit($url);
        $post_id = url_to_postid($url_with_trailing);
    }
    
    if (!$post_id) {
        $path = parse_url($url, PHP_URL_PATH);
        $slug = basename(rtrim($path, '/'));
        
        if (!empty($slug)) {
            global $wpdb;
            $post_id = $wpdb->get_var($wpdb->prepare(
                "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_status = 'publish' LIMIT 1",
                $slug
            ));
        }
    }
    
    if (!$post_id) {
        return new WP_Error('post_not_found', 'Could not find a post/page for this URL: ' . $url, array('status' => 404));
    }
    
    $post = get_post($post_id);
    
    if (!$post || $post->post_status !== 'publish') {
        return new WP_Error('post_not_found', 'Post not found or not published', array('status' => 404));
    }
    
    $image_ids = array();
    $image_relationships = array();
    
    // Featured image
    $featured_id = get_post_thumbnail_id($post_id);
    if ($featured_id) {
        $image_ids[] = intval($featured_id);
        $image_relationships[intval($featured_id)] = 'featured_image';
    }
    
    // Content images
    if (!empty($post->post_content)) {
        if (preg_match_all('/wp-image-(\d+)/', $post->post_content, $matches)) {
            foreach ($matches[1] as $img_id) {
                $img_id = intval($img_id);
                if (!in_array($img_id, $image_ids)) {
                    $image_ids[] = $img_id;
                    $image_relationships[$img_id] = 'in_content';
                } else if ($image_relationships[$img_id] === 'featured_image') {
                    $image_relationships[$img_id] = 'featured_and_content';
                }
            }
        }
    }
    
    // Page builder meta fields
    $meta_fields_to_check = array('_elementor_data', '_wpb_shortcodes_custom_css');
    foreach ($meta_fields_to_check as $meta_key) {
        $meta_value = get_post_meta($post_id, $meta_key, true);
        if (!empty($meta_value) && is_string($meta_value)) {
            if (preg_match_all('/wp-image-(\d+)/', $meta_value, $matches)) {
                foreach ($matches[1] as $img_id) {
                    $img_id = intval($img_id);
                    if (!in_array($img_id, $image_ids)) {
                        $image_ids[] = $img_id;
                        $image_relationships[$img_id] = 'in_page_builder';
                    }
                }
            }
            if (preg_match_all('/"id"\s*:\s*(\d+)/', $meta_value, $id_matches)) {
                foreach ($id_matches[1] as $potential_id) {
                    $potential_id = intval($potential_id);
                    if (wp_attachment_is_image($potential_id) && !in_array($potential_id, $image_ids)) {
                        $image_ids[] = $potential_id;
                        $image_relationships[$potential_id] = 'in_page_builder';
                    }
                }
            }
        }
    }
    
    if (empty($image_ids)) {
        return array(
            'url' => $url,
            'post_id' => $post_id,
            'post_title' => $post->post_title,
            'post_type' => $post->post_type,
            'focus_keyword' => get_post_meta($post_id, 'rank_math_focus_keyword', true),
            'message' => 'No images found on this page',
            'images' => array(),
            'summary' => array('total_images' => 0, 'average_score' => 0, 'images_needing_attention' => 0)
        );
    }
    
    $images = array();
    $total_score = 0;
    $needs_attention = 0;
    $issue_counts = array();
    
    foreach ($image_ids as $attachment_id) {
        $image_data = cortex_build_image_data($attachment_id, false);
        
        if (!$image_data) {
            $images[] = array(
                'id' => $attachment_id,
                'error' => 'Image not found (may have been deleted)',
                'relationship' => $image_relationships[$attachment_id] ?? 'unknown'
            );
            continue;
        }
        
        $image_data['relationship'] = $image_relationships[$attachment_id] ?? 'unknown';
        
        if ($include_preview) {
            $base64 = cortex_get_image_base64($attachment_id, $preview_size);
            if ($base64) {
                $image_data['preview'] = $base64;
                $image_data['preview_size'] = $preview_size;
            }
        }
        
        $total_score += $image_data['optimization_score'];
        
        if ($image_data['optimization_score'] < 70) {
            $needs_attention++;
        }
        
        foreach ($image_data['issues'] as $issue) {
            if (!isset($issue_counts[$issue])) $issue_counts[$issue] = 0;
            $issue_counts[$issue]++;
        }
        
        $images[] = $image_data;
    }
    
    arsort($issue_counts);
    
    $found_images = array_filter($images, function($img) { return !isset($img['error']); });
    $average_score = count($found_images) > 0 ? round($total_score / count($found_images), 1) : 0;
    
    return array(
        'url' => $url,
        'post_id' => $post_id,
        'post_title' => $post->post_title,
        'post_type' => $post->post_type,
        'focus_keyword' => get_post_meta($post_id, 'rank_math_focus_keyword', true),
        'images' => $images,
        'summary' => array(
            'total_images' => count($image_ids),
            'images_found' => count($found_images),
            'average_score' => $average_score,
            'images_needing_attention' => $needs_attention,
            'issue_counts' => $issue_counts
        ),
        'image_ids' => $image_ids
    );
}
function cortex_audit_images_by_url($request) {
    $url = $request->get_param('url');
    $include_preview = $request->get_param('include_preview') === 'true';
    $preview_size = $request->get_param('preview_size') ?: 'thumbnail';
    
    return cortex_audit_images_by_url_core($url, $include_preview, $preview_size);
}
function cortex_audit_images_by_url_post($request) {
    $url = $request->get_param('url');
    $include_preview = $request->get_param('include_preview') === true || $request->get_param('include_preview') === 'true';
    $preview_size = $request->get_param('preview_size') ?: 'thumbnail';
    
    return cortex_audit_images_by_url_core($url, $include_preview, $preview_size);
}
function cortex_audit_images($request) {
    $sample_limit = $request->get_param('sample_limit') 
        ? min(intval($request->get_param('sample_limit')), 100) 
        : CORTEX_AUDIT_SAMPLE_LIMIT;
    
    $in_use_ids = cortex_get_in_use_image_ids();
    
    $audit = array(
        'total_images' => count($in_use_ids),
        'scope' => 'in_use_only',
        'score_distribution' => array('excellent' => 0, 'good' => 0, 'needs_work' => 0, 'poor' => 0),
        'type_distribution' => array('content' => 0, 'decorative' => 0, 'ui' => 0),
        'issue_counts' => array(),
        'average_score' => 0,
        'images_needing_attention' => array(),
        'format_distribution' => array(),
        'total_file_size' => 0
    );
    
    if (empty($in_use_ids)) {
        $audit['message'] = 'No images found in use on published content';
        return $audit;
    }
    
    $total_score = 0;
    $needs_attention = array();
    
    foreach ($in_use_ids as $attachment_id) {
        $image_data = cortex_build_image_data($attachment_id, false);
        if (!$image_data) continue;
        
        $score = $image_data['optimization_score'];
        $total_score += $score;
        
        if ($score >= 90) $audit['score_distribution']['excellent']++;
        else if ($score >= 70) $audit['score_distribution']['good']++;
        else if ($score >= 50) $audit['score_distribution']['needs_work']++;
        else $audit['score_distribution']['poor']++;
        
        $type = $image_data['image_type'];
        if (isset($audit['type_distribution'][$type])) $audit['type_distribution'][$type]++;
        
        foreach ($image_data['issues'] as $issue) {
            if (!isset($audit['issue_counts'][$issue])) $audit['issue_counts'][$issue] = 0;
            $audit['issue_counts'][$issue]++;
        }
        
        $format = $image_data['mime_type'];
        if (!isset($audit['format_distribution'][$format])) $audit['format_distribution'][$format] = 0;
        $audit['format_distribution'][$format]++;
        
        $audit['total_file_size'] += $image_data['file_size'];
        
        if ($score < 70) {
            $needs_attention[] = array(
                'id' => $attachment_id,
                'url' => $image_data['url'],
                'filename' => $image_data['filename'],
                'score' => $score,
                'issues' => $image_data['issues'],
                'image_type' => $image_data['image_type']
            );
        }
    }
    
    usort($needs_attention, function($a, $b) { return $a['score'] - $b['score']; });
    $audit['images_needing_attention'] = array_slice($needs_attention, 0, $sample_limit);
    $audit['total_needing_attention'] = count($needs_attention);
    
    $audit['average_score'] = $audit['total_images'] > 0 ? round($total_score / $audit['total_images'], 1) : 0;
    $audit['total_file_size_formatted'] = size_format($audit['total_file_size']);
    
    arsort($audit['issue_counts']);
    
    // Recommendations
    $audit['recommendations'] = array();
    
    if (($audit['issue_counts']['missing_alt'] ?? 0) > 0) {
        $count = $audit['issue_counts']['missing_alt'];
        $audit['recommendations'][] = array(
            'priority' => 'high',
            'issue' => 'missing_alt',
            'count' => $count,
            'recommendation' => 'Add descriptive alt text (25-125 chars) with relevant keywords.'
        );
    }
    
    if (($audit['issue_counts']['not_webp'] ?? 0) > 0) {
        $count = $audit['issue_counts']['not_webp'];
        $audit['recommendations'][] = array(
            'priority' => 'medium',
            'issue' => 'not_webp',
            'count' => $count,
            'recommendation' => 'Convert to WebP format for better compression.'
        );
    }
    
    if (($audit['issue_counts']['file_too_large'] ?? 0) > 0) {
        $count = $audit['issue_counts']['file_too_large'];
        $audit['recommendations'][] = array(
            'priority' => 'high',
            'issue' => 'file_too_large',
            'count' => $count,
            'recommendation' => 'Compress images. Target <200KB standard, <500KB hero.'
        );
    }
    
    return $audit;
}
// ============================================================================
// RANKMATH SCHEMA ENHANCEMENT (Frontend Only)
// ============================================================================
if (!is_admin() && !wp_doing_ajax() && (!defined('REST_REQUEST') || !REST_REQUEST)) {
    add_filter('rank_math/json_ld', 'cortex_enhance_image_schema', 99, 2);
}
function cortex_enhance_image_schema($data, $jsonld) {
    if (!is_singular()) return $data;
    
    $post_id = get_the_ID();
    $site_name = get_bloginfo('name');
    $site_url = get_bloginfo('url');
    $current_year = date('Y');
    $license_page = $site_url . CORTEX_LICENSE_PAGE;
    
    $creator = array(
        '@type' => 'Organization',
        'name' => $site_name,
        'url' => $site_url
    );
    
    if (isset($data['@graph']) && is_array($data['@graph'])) {
        foreach ($data['@graph'] as &$item) {
            if (isset($item['@type']) && $item['@type'] === 'ImageObject') {
                cortex_add_licensing_fields($item, $site_name, $current_year, $creator, $license_page);
            }
        }
    }
    
    return $data;
}
function cortex_add_licensing_fields(&$image, $site_name, $year, $creator, $license_page) {
    if (!isset($image['creditText'])) $image['creditText'] = $site_name;
    if (!isset($image['copyrightNotice'])) $image['copyrightNotice'] = '© ' . $year . ' ' . $site_name;
    if (!isset($image['creator'])) $image['creator'] = $creator;
    if (!isset($image['acquireLicensePage'])) $image['acquireLicensePage'] = $license_page;
}

Comments

Add a Comment