$status)); } add_action('admin_menu', function () { add_menu_page('Pixlio – Image Editor', 'Pixlio – Image Editor', 'upload_files', 'pixlio-image-editor', function () { if (!current_user_can('upload_files')) { return; } echo '
'; }, 'dashicons-format-image', 100); }); add_action('admin_enqueue_scripts', function ($hook) { if ('toplevel_page_pixlio-image-editor' !== $hook) { return; } wp_enqueue_media(); wp_enqueue_style('pixlio-image-editor', plugins_url('assets/studio.css', __FILE__), array(), PIXLIO_IMAGE_EDITOR_VERSION . '.' . filemtime(__DIR__ . '/assets/studio.css')); wp_enqueue_script('pixlio-image-editor-engine', plugins_url('assets/engine.js', __FILE__), array(), PIXLIO_IMAGE_EDITOR_VERSION . '.' . filemtime(__DIR__ . '/assets/engine.js'), true); wp_enqueue_script('pixlio-image-editor', plugins_url('assets/studio-controls.js', __FILE__), array('pixlio-image-editor-engine'), PIXLIO_IMAGE_EDITOR_VERSION . '.' . filemtime(__DIR__ . '/assets/studio-controls.js'), true); wp_localize_script('pixlio-image-editor', 'PixlioConfig', array( 'rest' => esc_url_raw(rest_url('pixlio-image-editor/v1/')), 'nonce' => wp_create_nonce('wp_rest'), 'locale' => pixlio_image_editor_language(), 'strings' => pixlio_image_editor_strings(), 'defaults' => pixlio_image_editor_options(), 'maxBytes' => min(wp_max_upload_size(), 20 * MB_IN_BYTES), )); do_action('pixlio_image_editor_enqueue'); }); add_filter('all_plugins', function ($plugins) { $key = plugin_basename(__FILE__); if (isset($plugins[$key])) { $plugins[$key]['Description'] = pixlio_image_editor_label('pluginDescription'); } return $plugins; }); function pixlio_image_editor_permission($request) { if (!is_user_logged_in() || !current_user_can('upload_files') || !wp_verify_nonce($request->get_header('X-WP-Nonce'), 'wp_rest')) { return pixlio_image_editor_error('Your session expired or permission was denied. Reload the page.', 'セッション切れ、または権限がありません。画面を再読み込みしてください。', 403); } return true; } function pixlio_image_editor_attachment($id) { return get_post_type($id) === 'attachment' && wp_attachment_is_image($id) && current_user_can('edit_post', $id); } function pixlio_image_editor_revision($id) { return hash('sha256', (string) get_attached_file($id) . wp_json_encode(wp_get_attachment_metadata($id)) . (string) get_post_meta($id, '_image_studio_revision', true)); } function pixlio_image_editor_file($id) { if (!pixlio_image_editor_attachment($id)) { return pixlio_image_editor_error('Image not found or access denied.', '画像が見つからないか、編集権限がありません。', 403); } $attached = get_attached_file($id); $path = is_string($attached) && $attached !== '' ? realpath($attached) : false; $uploads = wp_upload_dir(); $base = realpath($uploads['basedir']); if (!$path || !$base || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0 || !is_file($path) || !is_readable($path)) { return pixlio_image_editor_error('This image is not available on local storage. Cloud-only media is not supported yet.', 'ローカルの画像ファイルが見つかりません。外部ストレージのみの画像には未対応です。'); } return $path; } function pixlio_image_editor_decode($data, $extra = array()) { if (!is_string($data) || strlen($data) > 28 * MB_IN_BYTES || !preg_match('#^data:image/(png|jpeg|webp);base64,([A-Za-z0-9+/=\r\n]+)$#D', $data, $m)) { return pixlio_image_editor_error('Invalid image data.', '画像データが不正です。'); } $allowed = array_merge(array('png','jpeg'), $extra); if (!in_array($m[1], $allowed, true)) { return pixlio_image_editor_error('Unsupported export format.', 'この書き出し形式には対応していません。'); } $bytes = base64_decode($m[2], true); if (!$bytes || strlen($bytes) > min(wp_max_upload_size(), 20 * MB_IN_BYTES)) { return pixlio_image_editor_error('Image exceeds the upload size limit.', '画像がアップロード容量の上限を超えています。'); } $info = @getimagesizefromstring($bytes); if (!$info || $info['mime'] !== 'image/' . $m[1] || $info[0] > 8192 || $info[1] > 8192 || $info[0] * $info[1] > 24000000) { return pixlio_image_editor_error('Invalid image or dimensions exceed 8192 px / 24 MP.', '画像が不正、または8192px・2400万画素の上限を超えています。'); } return array('bytes' => $bytes, 'mime' => $info['mime'], 'ext' => $m[1] === 'jpeg' ? 'jpg' : $m[1]); } function pixlio_image_editor_write($decoded, $name) { require_once ABSPATH . 'wp-admin/includes/file.php'; $temp = wp_tempnam('pixlio-image-editor'); if (!$temp || file_put_contents($temp, $decoded['bytes']) !== strlen($decoded['bytes'])) { if ($temp) { wp_delete_file($temp); } return pixlio_image_editor_error('Unable to write image. Check free disk space.', '画像を書き込めません。空き容量を確認してください。', 500); } $safe_name = sanitize_file_name($name); if ($safe_name === '') { $safe_name = 'pixlio-image'; } $file = array('name' => $safe_name . '.' . $decoded['ext'], 'tmp_name' => $temp, 'error' => 0, 'size' => strlen($decoded['bytes'])); $result = wp_handle_sideload($file, array('test_form' => false, 'mimes' => array('png'=>'image/png','jpg'=>'image/jpeg','webp'=>'image/webp'))); if (file_exists($temp)) { wp_delete_file($temp); } return isset($result['error']) ? pixlio_image_editor_error($result['error'], $result['error'], 500) : $result; } function pixlio_image_editor_new_image($decoded, $title) { require_once ABSPATH . 'wp-admin/includes/image.php'; $file = pixlio_image_editor_write($decoded, $title); if (is_wp_error($file)) { return $file; } $id = wp_insert_attachment(array('post_mime_type'=>$decoded['mime'], 'post_title'=>sanitize_text_field($title), 'post_status'=>'inherit'), $file['file'], 0, true); if (is_wp_error($id)) { wp_delete_file($file['file']); return $id; } // Preserve the requested export dimensions; WordPress normally scales large uploads. $keep_dimensions = function () { return false; }; add_filter('big_image_size_threshold', $keep_dimensions, 999); try { $meta = wp_generate_attachment_metadata($id, $file['file']); } finally { remove_filter('big_image_size_threshold', $keep_dimensions, 999); } if (empty($meta['width']) || empty($meta['height'])) { wp_delete_attachment($id, true); return pixlio_image_editor_error('Image metadata generation failed.', '画像情報の生成に失敗しました。', 500); } // Sub-size generation can already persist the complete metadata. WordPress // returns false for an unchanged value as well as for a database failure. $metadata_updated = wp_update_attachment_metadata($id, $meta); $stored_metadata = wp_get_attachment_metadata($id, true); if (false === $metadata_updated && (!is_array($stored_metadata) || $stored_metadata != $meta)) { wp_delete_attachment($id, true); return pixlio_image_editor_error('Unable to save image metadata.', '画像情報を保存できませんでした。', 500); } update_post_meta($id, '_image_studio_edited', 1); return $id; } add_action('rest_api_init', function () { register_rest_route('pixlio-image-editor/v1', '/images', array('args'=>array( 'page'=>array('type'=>'integer','minimum'=>1,'maximum'=>100000,'default'=>1), 'sort'=>array('type'=>'string','enum'=>array('newest','oldest','title'),'default'=>'newest'), 'search'=>array('type'=>'string','maxLength'=>200,'default'=>''), 'edited'=>array('type'=>'string','enum'=>array('','0','1'),'default'=>'') ),'methods'=>'GET', 'permission_callback'=>'pixlio_image_editor_permission', 'callback'=>function ($r) { $sort = $r->get_param('sort'); $page = max(1, absint($r->get_param('page'))); if (!is_null($r->get_param('search')) && !is_string($r->get_param('search'))) { return pixlio_image_editor_error('Invalid search.', '検索文字列が不正です。'); } $args = array('post_type'=>'attachment', 'post_status'=>'inherit', 'post_mime_type'=>'image', 'posts_per_page'=>24, 'paged'=>$page, 's'=>sanitize_text_field((string)$r->get_param('search')), 'orderby'=>$sort === 'title' ? 'title' : 'date', 'order'=>$sort === 'oldest' || $sort === 'title' ? 'ASC' : 'DESC'); if (!current_user_can('edit_others_posts')) { $args['author'] = get_current_user_id(); } if ($r->get_param('edited')) { $args['meta_key'] = '_image_studio_edited'; $args['meta_value'] = 1; } $q = new WP_Query($args); $items = array(); foreach ($q->posts as $p) { if (!pixlio_image_editor_attachment($p->ID)) { continue; } $m = wp_get_attachment_metadata($p->ID); $items[] = array('id'=>$p->ID, 'title'=>get_the_title($p->ID), 'thumb'=>wp_get_attachment_image_url($p->ID, 'medium'), 'width'=>$m['width'] ?? 0, 'height'=>$m['height'] ?? 0, 'mime'=>$p->post_mime_type, 'edited'=>(bool)get_post_meta($p->ID, '_image_studio_edited', true)); } return array('items'=>$items, 'pages'=>(int)$q->max_num_pages); })); register_rest_route('pixlio-image-editor/v1', '/image/(?P