tigerstyle-heat/includes/modules/class-gltf-metadata.php
Ryan Malloy 0028738e33 Initial commit: TigerStyle Heat v2.0.0
Make your WordPress site irresistible. Natural SEO attraction with:
- robots.txt management
- sitemap.xml generation
- LLMs.txt support
- Google integration (Analytics, Search Console, Tag Manager)
- Schema.org structured data
- Open Graph / Twitter Card meta tags
- AMP support
- Visual elements gallery
- Built-in backup/restore module

Includes build.sh and .distignore for WordPress-installable release ZIPs.
2026-05-27 13:41:35 -06:00

742 lines
25 KiB
PHP

<?php
/**
* glTF 3D Asset Metadata Extraction Module for TigerStyle Heat
* Implements glTF 2.0 file format detection, validation, and metadata extraction
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class TigerStyleSEO_glTF_Metadata {
/**
* Single instance
*/
private static $instance = null;
/**
* Supported glTF file extensions
*/
private $supported_extensions = array('gltf', 'glb');
/**
* glTF MIME types
*/
private $mime_types = array(
'gltf' => 'model/gltf+json',
'glb' => 'model/gltf-binary'
);
/**
* Get instance
*/
public static function instance() {
if (is_null(self::$instance)) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init();
}
/**
* Initialize the module
*/
private function init() {
// Media handling hooks
add_filter('wp_check_filetype_and_ext', array($this, 'check_gltf_filetype'), 10, 4);
add_filter('upload_mimes', array($this, 'add_gltf_mime_types'));
add_action('add_attachment', array($this, 'process_gltf_attachment'));
add_action('edit_attachment', array($this, 'process_gltf_attachment'));
// Admin hooks
if (is_admin()) {
add_filter('attachment_fields_to_edit', array($this, 'add_gltf_attachment_fields'), 10, 2);
add_filter('attachment_fields_to_save', array($this, 'save_gltf_attachment_fields'), 10, 2);
}
// Frontend hooks
add_filter('wp_get_attachment_metadata', array($this, 'enhance_gltf_metadata'), 10, 2);
}
/**
* Check if file is a valid glTF file
*/
public function check_gltf_filetype($data, $file, $filename, $mimes) {
error_log('TigerStyle Heat: check_gltf_filetype called for: ' . $filename);
$wp_filetype = wp_check_filetype($filename, $mimes);
$ext = $wp_filetype['ext'];
$type = $wp_filetype['type'];
error_log('TigerStyle Heat: wp_check_filetype ext=' . $ext . ', type=' . $type);
if (in_array($ext, $this->supported_extensions)) {
error_log('TigerStyle Heat: File extension supported, validating: ' . $file);
// Validate glTF file structure - pass the extension since temp files don't have extensions
if ($this->validate_gltf_file_with_extension($file, $ext)) {
error_log('TigerStyle Heat: File validation passed');
$data['ext'] = $ext;
$data['type'] = $this->mime_types[$ext];
} else {
error_log('TigerStyle Heat: File validation failed');
// Invalid glTF file
$data['ext'] = false;
$data['type'] = false;
}
} else {
error_log('TigerStyle Heat: File extension not supported: ' . $ext);
}
error_log('TigerStyle Heat: Final data: ' . print_r($data, true));
return $data;
}
/**
* Add glTF MIME types to WordPress
*/
public function add_gltf_mime_types($mimes) {
// Debug logging
error_log('TigerStyle Heat: Adding glTF MIME types');
$mimes['gltf'] = 'model/gltf+json';
$mimes['glb'] = 'model/gltf-binary';
// Log the updated mimes array
error_log('TigerStyle Heat: Updated MIME types: ' . print_r($mimes, true));
return $mimes;
}
/**
* Validate glTF file structure with known extension
*/
private function validate_gltf_file_with_extension($file_path, $extension) {
error_log('TigerStyle Heat: validate_gltf_file_with_extension called with: ' . $file_path . ', ext: ' . $extension);
if (!file_exists($file_path)) {
error_log('TigerStyle Heat: File does not exist: ' . $file_path);
return false;
}
if ($extension === 'gltf') {
error_log('TigerStyle Heat: Validating as glTF JSON');
return $this->validate_gltf_json($file_path);
} elseif ($extension === 'glb') {
error_log('TigerStyle Heat: Validating as GLB binary');
return $this->validate_glb_binary($file_path);
}
error_log('TigerStyle Heat: Unknown extension: ' . $extension);
return false;
}
/**
* Validate glTF file structure (legacy method - kept for compatibility)
*/
private function validate_gltf_file($file_path) {
error_log('TigerStyle Heat: validate_gltf_file called with: ' . $file_path);
if (!file_exists($file_path)) {
error_log('TigerStyle Heat: File does not exist: ' . $file_path);
return false;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
error_log('TigerStyle Heat: File extension detected: ' . $extension);
if ($extension === 'gltf') {
error_log('TigerStyle Heat: Validating as glTF JSON');
return $this->validate_gltf_json($file_path);
} elseif ($extension === 'glb') {
error_log('TigerStyle Heat: Validating as GLB binary');
return $this->validate_glb_binary($file_path);
}
error_log('TigerStyle Heat: Unknown extension: ' . $extension);
return false;
}
/**
* Validate glTF JSON file
*/
private function validate_gltf_json($file_path) {
$content = file_get_contents($file_path);
if ($content === false) {
return false;
}
$json = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return false;
}
// Check for required glTF structure
return $this->validate_gltf_structure($json);
}
/**
* Validate GLB binary file
*/
private function validate_glb_binary($file_path) {
error_log('TigerStyle Heat: validate_glb_binary called');
$handle = fopen($file_path, 'rb');
if (!$handle) {
error_log('TigerStyle Heat: Could not open file for reading');
return false;
}
// Read GLB header (12 bytes)
$header = fread($handle, 12);
if (strlen($header) < 12) {
error_log('TigerStyle Heat: Header too short: ' . strlen($header) . ' bytes');
fclose($handle);
return false;
}
error_log('TigerStyle Heat: Header read successfully, length: ' . strlen($header));
// Check magic number (first 4 bytes should be "glTF")
$magic = substr($header, 0, 4);
if ($magic !== 'glTF') {
fclose($handle);
return false;
}
// Check version (bytes 4-7, should be 2 for glTF 2.0)
$version = unpack('V', substr($header, 4, 4))[1];
if ($version !== 2) {
fclose($handle);
return false;
}
fclose($handle);
return true;
}
/**
* Validate glTF JSON structure
*/
private function validate_gltf_structure($json) {
// Check required properties
if (!isset($json['asset'])) {
return false;
}
// Check asset version
if (!isset($json['asset']['version']) || $json['asset']['version'] !== '2.0') {
return false;
}
// Basic structure validation
$required_arrays = array('scenes', 'nodes', 'meshes', 'accessors', 'bufferViews', 'buffers');
foreach ($required_arrays as $array_name) {
if (isset($json[$array_name]) && !is_array($json[$array_name])) {
return false;
}
}
return true;
}
/**
* Extract metadata from glTF file
*/
public function extract_gltf_metadata($file_path) {
if (!$this->validate_gltf_file($file_path)) {
return false;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
$metadata = array(
'file_format' => $extension,
'mime_type' => $this->mime_types[$extension],
'file_size' => filesize($file_path),
'version' => '2.0'
);
if ($extension === 'gltf') {
$gltf_data = $this->extract_gltf_json_metadata($file_path);
} else {
$gltf_data = $this->extract_glb_metadata($file_path);
}
return array_merge($metadata, $gltf_data);
}
/**
* Extract metadata from glTF JSON file
*/
private function extract_gltf_json_metadata($file_path) {
$content = file_get_contents($file_path);
$json = json_decode($content, true);
if (!$json) {
return array();
}
return $this->parse_gltf_json($json);
}
/**
* Extract metadata from GLB binary file
*/
private function extract_glb_metadata($file_path) {
$handle = fopen($file_path, 'rb');
if (!$handle) {
return array();
}
// Skip header (12 bytes)
fseek($handle, 12);
// Read first chunk header (8 bytes)
$chunk_header = fread($handle, 8);
if (strlen($chunk_header) < 8) {
fclose($handle);
return array();
}
$chunk_length = unpack('V', substr($chunk_header, 0, 4))[1];
$chunk_type = substr($chunk_header, 4, 4);
// First chunk should be JSON
if ($chunk_type !== 'JSON') {
fclose($handle);
return array();
}
// Read JSON data
$json_data = fread($handle, $chunk_length);
fclose($handle);
$json = json_decode($json_data, true);
if (!$json) {
return array();
}
return $this->parse_gltf_json($json);
}
/**
* Parse glTF JSON structure and extract metadata
*/
private function parse_gltf_json($json) {
$metadata = array();
// Asset information
if (isset($json['asset'])) {
$asset = $json['asset'];
$metadata['version'] = $asset['version'] ?? '2.0';
$metadata['generator'] = $asset['generator'] ?? '';
$metadata['copyright'] = $asset['copyright'] ?? '';
$metadata['min_version'] = $asset['minVersion'] ?? '';
}
// Scene information
$metadata['scene_count'] = isset($json['scenes']) ? count($json['scenes']) : 0;
$metadata['node_count'] = isset($json['nodes']) ? count($json['nodes']) : 0;
// Mesh information
$metadata['mesh_count'] = isset($json['meshes']) ? count($json['meshes']) : 0;
$metadata['primitive_count'] = $this->count_primitives($json);
$metadata['vertex_count'] = $this->estimate_vertex_count($json);
// Material information
$metadata['material_count'] = isset($json['materials']) ? count($json['materials']) : 0;
$metadata['texture_count'] = isset($json['textures']) ? count($json['textures']) : 0;
$metadata['image_count'] = isset($json['images']) ? count($json['images']) : 0;
// Animation information
$metadata['animation_count'] = isset($json['animations']) ? count($json['animations']) : 0;
$metadata['has_animations'] = $metadata['animation_count'] > 0;
// Extension information
$metadata['extensions_used'] = $json['extensionsUsed'] ?? array();
$metadata['extensions_required'] = $json['extensionsRequired'] ?? array();
// Buffer information
$metadata['buffer_count'] = isset($json['buffers']) ? count($json['buffers']) : 0;
$metadata['total_buffer_size'] = $this->calculate_total_buffer_size($json);
// Accessor information
$metadata['accessor_count'] = isset($json['accessors']) ? count($json['accessors']) : 0;
$metadata['buffer_view_count'] = isset($json['bufferViews']) ? count($json['bufferViews']) : 0;
// Camera information
$metadata['camera_count'] = isset($json['cameras']) ? count($json['cameras']) : 0;
// Light information (if KHR_lights_punctual extension is used)
if (isset($json['extensions']['KHR_lights_punctual']['lights'])) {
$metadata['light_count'] = count($json['extensions']['KHR_lights_punctual']['lights']);
} else {
$metadata['light_count'] = 0;
}
// Complexity assessment
$metadata['complexity_score'] = $this->calculate_complexity_score($metadata);
$metadata['complexity_level'] = $this->get_complexity_level($metadata['complexity_score']);
return $metadata;
}
/**
* Count total primitives across all meshes
*/
private function count_primitives($json) {
$total = 0;
if (isset($json['meshes'])) {
foreach ($json['meshes'] as $mesh) {
if (isset($mesh['primitives'])) {
$total += count($mesh['primitives']);
}
}
}
return $total;
}
/**
* Estimate total vertex count
*/
private function estimate_vertex_count($json) {
$total = 0;
if (isset($json['meshes']) && isset($json['accessors'])) {
foreach ($json['meshes'] as $mesh) {
if (isset($mesh['primitives'])) {
foreach ($mesh['primitives'] as $primitive) {
if (isset($primitive['attributes']['POSITION'])) {
$accessor_index = $primitive['attributes']['POSITION'];
if (isset($json['accessors'][$accessor_index])) {
$total += $json['accessors'][$accessor_index]['count'] ?? 0;
}
}
}
}
}
}
return $total;
}
/**
* Calculate total buffer size
*/
private function calculate_total_buffer_size($json) {
$total = 0;
if (isset($json['buffers'])) {
foreach ($json['buffers'] as $buffer) {
$total += $buffer['byteLength'] ?? 0;
}
}
return $total;
}
/**
* Calculate complexity score for the 3D model
*/
private function calculate_complexity_score($metadata) {
$score = 0;
// Mesh complexity
$score += $metadata['mesh_count'] * 10;
$score += $metadata['primitive_count'] * 5;
$score += ($metadata['vertex_count'] / 1000) * 2; // Per thousand vertices
// Material complexity
$score += $metadata['material_count'] * 8;
$score += $metadata['texture_count'] * 12;
// Animation complexity
$score += $metadata['animation_count'] * 15;
// Extension complexity
$score += count($metadata['extensions_used']) * 5;
$score += count($metadata['extensions_required']) * 10;
// Buffer size impact
$score += ($metadata['total_buffer_size'] / (1024 * 1024)) * 3; // Per MB
return round($score);
}
/**
* Get complexity level based on score
*/
private function get_complexity_level($score) {
if ($score < 50) {
return 'Low';
} elseif ($score < 150) {
return 'Medium';
} elseif ($score < 300) {
return 'High';
} else {
return 'Very High';
}
}
/**
* Process glTF attachment and extract metadata
*/
public function process_gltf_attachment($attachment_id) {
$file_path = get_attached_file($attachment_id);
if (!$file_path) {
return;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if (!in_array($extension, $this->supported_extensions)) {
return;
}
// Extract glTF metadata
$gltf_metadata = $this->extract_gltf_metadata($file_path);
if ($gltf_metadata) {
// Store metadata as post meta
foreach ($gltf_metadata as $key => $value) {
update_post_meta($attachment_id, '_gltf_' . $key, $value);
}
// Store structured metadata
update_post_meta($attachment_id, '_gltf_metadata', $gltf_metadata);
// Set attachment as 3D model
update_post_meta($attachment_id, '_wp_attachment_image_alt', '3D Model');
}
}
/**
* Enhance WordPress attachment metadata with glTF data
*/
public function enhance_gltf_metadata($metadata, $attachment_id) {
$file_path = get_attached_file($attachment_id);
if (!$file_path) {
return $metadata;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if (!in_array($extension, $this->supported_extensions)) {
return $metadata;
}
// Get cached glTF metadata
$gltf_metadata = get_post_meta($attachment_id, '_gltf_metadata', true);
if (!$gltf_metadata) {
// Extract and cache metadata
$gltf_metadata = $this->extract_gltf_metadata($file_path);
if ($gltf_metadata) {
update_post_meta($attachment_id, '_gltf_metadata', $gltf_metadata);
}
}
if ($gltf_metadata) {
$metadata['gltf'] = $gltf_metadata;
}
return $metadata;
}
/**
* Add glTF-specific fields to attachment edit screen
*/
public function add_gltf_attachment_fields($form_fields, $post) {
$file_path = get_attached_file($post->ID);
if (!$file_path) {
return $form_fields;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
if (!in_array($extension, $this->supported_extensions)) {
return $form_fields;
}
$gltf_metadata = get_post_meta($post->ID, '_gltf_metadata', true);
if (!$gltf_metadata) {
return $form_fields;
}
// Add 3D model information fields
$form_fields['gltf_info'] = array(
'label' => __('3D Model Information', 'tigerstyle-heat'),
'input' => 'html',
'html' => $this->render_gltf_info_html($gltf_metadata),
'helps' => __('Technical information about this glTF 3D model.', 'tigerstyle-heat')
);
// Add custom license field
$license = get_post_meta($post->ID, '_gltf_license', true);
$form_fields['gltf_license'] = array(
'label' => __('3D Model License', 'tigerstyle-heat'),
'input' => 'text',
'value' => $license,
'helps' => __('License information for this 3D model (e.g., CC BY 4.0, MIT, etc.)', 'tigerstyle-heat')
);
// Add creator field
$creator = get_post_meta($post->ID, '_gltf_creator', true);
$form_fields['gltf_creator'] = array(
'label' => __('3D Model Creator', 'tigerstyle-heat'),
'input' => 'text',
'value' => $creator,
'helps' => __('Name of the person or organization who created this 3D model.', 'tigerstyle-heat')
);
return $form_fields;
}
/**
* Save glTF-specific attachment fields
*/
public function save_gltf_attachment_fields($post, $attachment) {
if (isset($attachment['gltf_license'])) {
update_post_meta($post['ID'], '_gltf_license', sanitize_text_field($attachment['gltf_license']));
}
if (isset($attachment['gltf_creator'])) {
update_post_meta($post['ID'], '_gltf_creator', sanitize_text_field($attachment['gltf_creator']));
}
return $post;
}
/**
* Render glTF information HTML for admin
*/
private function render_gltf_info_html($metadata) {
ob_start();
?>
<div style="background: #f9f9f9; padding: 10px; border: 1px solid #ddd; border-radius: 4px;">
<h4 style="margin-top: 0;"><?php _e('glTF Technical Details', 'tigerstyle-heat'); ?></h4>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<strong><?php _e('Format:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['file_format']); ?><br>
<strong><?php _e('Version:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['version']); ?><br>
<strong><?php _e('File Size:', 'tigerstyle-heat'); ?></strong> <?php echo size_format($metadata['file_size']); ?><br>
<strong><?php _e('Complexity:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['complexity_level']); ?>
</div>
<div>
<strong><?php _e('Meshes:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['mesh_count']); ?><br>
<strong><?php _e('Materials:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['material_count']); ?><br>
<strong><?php _e('Textures:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['texture_count']); ?><br>
<strong><?php _e('Animations:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['animation_count']); ?>
</div>
</div>
<?php if (!empty($metadata['vertex_count'])): ?>
<p><strong><?php _e('Vertices:', 'tigerstyle-heat'); ?></strong> <?php echo number_format($metadata['vertex_count']); ?></p>
<?php endif; ?>
<?php if (!empty($metadata['generator'])): ?>
<p><strong><?php _e('Generated by:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html($metadata['generator']); ?></p>
<?php endif; ?>
<?php if (!empty($metadata['extensions_used'])): ?>
<p><strong><?php _e('Extensions Used:', 'tigerstyle-heat'); ?></strong> <?php echo esc_html(implode(', ', $metadata['extensions_used'])); ?></p>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Get structured data for glTF 3D model
*/
public function get_gltf_structured_data($attachment_id) {
$post = get_post($attachment_id);
if (!$post || $post->post_type !== 'attachment') {
return null;
}
$file_path = get_attached_file($attachment_id);
$file_url = wp_get_attachment_url($attachment_id);
$gltf_metadata = get_post_meta($attachment_id, '_gltf_metadata', true);
if (!$file_url || !$gltf_metadata) {
return null;
}
$schema = array(
'@type' => '3DModel',
'contentUrl' => $file_url,
'encodingFormat' => $gltf_metadata['mime_type'],
'name' => $post->post_title ?: basename($file_url),
'description' => $post->post_content,
'caption' => $post->post_excerpt
);
// Add technical metadata
$schema['fileSize'] = $gltf_metadata['file_size'];
$schema['fileFormat'] = $gltf_metadata['file_format'];
// Add 3D-specific properties
if (isset($gltf_metadata['mesh_count'])) {
$schema['additionalProperty'] = array(
array(
'@type' => 'PropertyValue',
'name' => 'meshCount',
'value' => $gltf_metadata['mesh_count']
),
array(
'@type' => 'PropertyValue',
'name' => 'materialCount',
'value' => $gltf_metadata['material_count']
),
array(
'@type' => 'PropertyValue',
'name' => 'complexityLevel',
'value' => $gltf_metadata['complexity_level']
)
);
}
// Add license information
$license = get_post_meta($attachment_id, '_gltf_license', true);
if ($license) {
$schema['license'] = $license;
}
// Add creator information
$creator = get_post_meta($attachment_id, '_gltf_creator', true);
if ($creator) {
$schema['creator'] = array(
'@type' => 'Person',
'name' => $creator
);
}
return $schema;
}
/**
* Check if attachment is a glTF 3D model
*/
public function is_gltf_attachment($attachment_id) {
$file_path = get_attached_file($attachment_id);
if (!$file_path) {
return false;
}
$extension = strtolower(pathinfo($file_path, PATHINFO_EXTENSION));
return in_array($extension, $this->supported_extensions);
}
/**
* Get glTF metadata for attachment
*/
public function get_gltf_metadata($attachment_id) {
if (!$this->is_gltf_attachment($attachment_id)) {
return null;
}
return get_post_meta($attachment_id, '_gltf_metadata', true);
}
}