Source: Providers/Azure/ComputerVision.php

<?php
/**
 * Azure AI Vision
 */

namespace Classifai\Providers\Azure;

use Classifai\Features\DescriptiveTextGenerator;
use Classifai\Features\ImageTagsGenerator;
use Classifai\Features\ImageTextExtraction;
use Classifai\Features\PDFTextExtraction;
use Classifai\Features\ImageCropping;
use Classifai\Providers\Azure\SmartCropping;
use Classifai\Providers\Provider;
use WP_Error;

use function Classifai\computer_vision_max_filesize;
use function Classifai\get_largest_acceptable_image_url;
use function Classifai\get_modified_image_source_url;

class ComputerVision extends Provider {

	const ID = 'ms_computer_vision';

	/**
	 * @var string URL fragment to the analyze API endpoint
	 */
	protected $analyze_url = 'vision/v3.2/analyze';

	/**
	 * ComputerVision constructor.
	 *
	 * @param \Classifai\Features\Feature $feature_instance The feature instance.
	 */
	public function __construct( $feature_instance = null ) {
		$this->feature_instance = $feature_instance;
	}

	/**
	 * Renders the provider fields.
	 */
	public function render_provider_fields() {
		$settings = $this->feature_instance->get_settings( static::ID );

		add_settings_field(
			static::ID . '_endpoint_url',
			esc_html__( 'Endpoint URL', 'classifai' ),
			[ $this->feature_instance, 'render_input' ],
			$this->feature_instance->get_option_name(),
			$this->feature_instance->get_option_name() . '_section',
			[
				'option_index'  => static::ID,
				'label_for'     => 'endpoint_url',
				'input_type'    => 'text',
				'default_value' => $settings['endpoint_url'],
				'description'   => __( 'Supported protocol and hostname endpoints, e.g., <code>https://REGION.api.cognitive.microsoft.com</code> or <code>https://EXAMPLE.cognitiveservices.azure.com</code>. This can look different based on your setting choices in Azure.', 'classifai' ),
				'class'         => 'large-text classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this.
			]
		);

		add_settings_field(
			static::ID . '_api_key',
			esc_html__( 'API Key', 'classifai' ),
			[ $this->feature_instance, 'render_input' ],
			$this->feature_instance->get_option_name(),
			$this->feature_instance->get_option_name() . '_section',
			[
				'option_index'  => static::ID,
				'label_for'     => 'api_key',
				'input_type'    => 'password',
				'default_value' => $settings['api_key'],
				'class'         => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this.
			]
		);

		switch ( $this->feature_instance::ID ) {
			case DescriptiveTextGenerator::ID:
				$this->add_descriptive_text_generation_fields();
				break;

			case ImageTagsGenerator::ID:
				$this->add_image_tags_generation_fields();
				break;
		}

		/**
		 * Allows more Provider specific settings to be rendered.
		 *
		 * @since 3.0.0
		 * @hook classifai_ms_computer_vision_render_provider_fields
		 *
		 * @param {object} $this The Provider object.
		 */
		do_action( 'classifai_' . static::ID . '_render_provider_fields', $this );
	}

	/**
	 * Renders fields for the Descriptive Text Feature.
	 */
	public function add_descriptive_text_generation_fields() {
		$settings = $this->feature_instance->get_settings( static::ID );

		add_settings_field(
			static::ID . '_descriptive_confidence_threshold',
			esc_html__( 'Confidence threshold', 'classifai' ),
			[ $this->feature_instance, 'render_input' ],
			$this->feature_instance->get_option_name(),
			$this->feature_instance->get_option_name() . '_section',
			[
				'option_index'  => static::ID,
				'label_for'     => 'descriptive_confidence_threshold',
				'input_type'    => 'number',
				'min'           => 1,
				'step'          => 1,
				'default_value' => $settings['descriptive_confidence_threshold'],
				'description'   => esc_html__( 'Minimum confidence score for automatically added generated text, numeric value from 0-100. Recommended to be set to at least 55.', 'classifai' ),
				'class'         => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this.
			]
		);
	}

	/**
	 * Renders fields for the Image Tags Feature.
	 */
	public function add_image_tags_generation_fields() {
		$settings = $this->feature_instance->get_settings( static::ID );

		add_settings_field(
			static::ID . '_tag_confidence_threshold',
			esc_html__( 'Confidence threshold', 'classifai' ),
			[ $this->feature_instance, 'render_input' ],
			$this->feature_instance->get_option_name(),
			$this->feature_instance->get_option_name() . '_section',
			[
				'option_index'  => static::ID,
				'label_for'     => 'tag_confidence_threshold',
				'input_type'    => 'number',
				'min'           => 1,
				'step'          => 1,
				'default_value' => $settings['tag_confidence_threshold'],
				'description'   => esc_html__( 'Minimum confidence score for automatically added image tags, numeric value from 0-100. Recommended to be set to at least 70.', 'classifai' ),
				'class'         => 'classifai-provider-field hidden provider-scope-' . static::ID, // Important to add this.
			]
		);
	}

	/**
	 * Returns the default settings for the current provider
	 * and the settings needed for the feature which uses this provider.
	 *
	 * @return array
	 */
	public function get_default_provider_settings(): array {
		$common_settings = [
			'endpoint_url'  => '',
			'api_key'       => '',
			'authenticated' => false,
		];

		switch ( $this->feature_instance::ID ) {
			case DescriptiveTextGenerator::ID:
				return array_merge(
					$common_settings,
					[
						'descriptive_confidence_threshold' => 55,
					]
				);

			case ImageTagsGenerator::ID:
				return array_merge(
					$common_settings,
					[
						'tag_confidence_threshold' => 70,
					]
				);
		}

		return $common_settings;
	}

	/**
	 * Sanitization
	 *
	 * @param array $new_settings The settings being saved.
	 * @return array|mixed
	 */
	public function sanitize_settings( array $new_settings ) {
		$settings = $this->feature_instance->get_settings();

		if ( ! empty( $new_settings[ static::ID ]['endpoint_url'] ) && ! empty( $new_settings[ static::ID ]['api_key'] ) ) {
			$new_settings[ static::ID ]['authenticated'] = $settings[ static::ID ]['authenticated'];
			$new_settings[ static::ID ]['endpoint_url']  = esc_url_raw( $new_settings[ static::ID ]['endpoint_url'] ?? $settings[ static::ID ]['endpoint_url'] );
			$new_settings[ static::ID ]['api_key']       = sanitize_text_field( $new_settings[ static::ID ]['api_key'] ?? $settings[ static::ID ]['api_key'] );

			$is_authenticated = $new_settings[ static::ID ]['authenticated'];
			$is_endpoint_same = $new_settings[ static::ID ]['endpoint_url'] === $settings[ static::ID ]['endpoint_url'];
			$is_api_key_same  = $new_settings[ static::ID ]['api_key'] === $settings[ static::ID ]['api_key'];

			if ( ! ( $is_authenticated && $is_endpoint_same && $is_api_key_same ) ) {
				$auth_check = $this->authenticate_credentials(
					$new_settings[ static::ID ]['endpoint_url'],
					$new_settings[ static::ID ]['api_key']
				);

				if ( is_wp_error( $auth_check ) ) {
					$new_settings[ static::ID ]['authenticated'] = false;
				} else {
					$new_settings[ static::ID ]['authenticated'] = true;
				}
			}
		} else {
			$new_settings[ static::ID ]['endpoint_url'] = $settings[ static::ID ]['endpoint_url'];
			$new_settings[ static::ID ]['api_key']      = $settings[ static::ID ]['api_key'];
		}

		if ( $this->feature_instance instanceof DescriptiveTextGenerator ) {
			$new_settings[ static::ID ]['descriptive_confidence_threshold'] = absint( $new_settings[ static::ID ]['descriptive_confidence_threshold'] ?? $settings[ static::ID ]['descriptive_confidence_threshold'] );
		}

		if ( $this->feature_instance instanceof ImageTagsGenerator ) {
			$new_settings[ static::ID ]['tag_confidence_threshold'] = absint( $new_settings[ static::ID ]['tag_confidence_threshold'] ?? $settings[ static::ID ]['tag_confidence_threshold'] );
		}

		return $new_settings;
	}

	/**
	 * Register the functionality.
	 */
	public function register() {
		add_action( 'classifai_retry_get_read_result', [ $this, 'do_read_cron' ], 10, 2 );
		add_action( 'wp_ajax_classifai_get_read_status', [ $this, 'get_read_status_ajax' ] );
		add_filter( 'classifai_feature_pdf_to_text_generation_read_status', [ $this, 'get_read_status' ], 10, 2 );
		add_filter( 'posts_clauses', [ $this, 'filter_attachment_query_keywords' ], 10, 1 );
	}

	/**
	 * Callback to get the status of the PDF read with AJAX support.
	 */
	public static function get_read_status_ajax() {
		if ( ! wp_doing_ajax() ) {
			return;
		}

		// Nonce check.
		if ( ! check_ajax_referer( 'classifai', 'nonce', false ) ) {
			$error = new \WP_Error( 'classifai_nonce_error', __( 'Nonce could not be verified.', 'classifai' ) );
			wp_send_json_error( $error );
			exit();
		}

		// Attachment ID check.
		$attachment_id = filter_input( INPUT_POST, 'attachment_id', FILTER_SANITIZE_NUMBER_INT );
		if ( empty( $attachment_id ) ) {
			$error = new \WP_Error( 'invalid_post', __( 'Invalid attachment ID.', 'classifai' ) );
			wp_send_json_error( $error );
			exit();
		}

		// User capability check.
		if ( ! current_user_can( 'edit_post', $attachment_id ) ) {
			$error = new \WP_Error( 'unauthorized_access', __( 'Unauthorized access.', 'classifai' ) );
			wp_send_json_error( $error );
			exit();
		}

		wp_send_json_success( self::get_read_status( [], $attachment_id ) );
	}

	/**
	 * Callback to get the status of the PDF read.
	 *
	 * @param array $status Current status.
	 * @param int   $attachment_id The attachment ID.
	 * @return array Read and running status.
	 */
	public static function get_read_status( array $status = [], $attachment_id = null ) {
		if ( empty( $attachment_id ) || ! is_numeric( $attachment_id ) ) {
			return $status;
		}

		$attachment_id = (int) $attachment_id;

		$read    = ! empty( get_the_content( null, false, $attachment_id ) );
		$status  = get_post_meta( $attachment_id, '_classifai_azure_read_status', true );
		$running = ( ! empty( $status['status'] ) && 'running' === $status['status'] );

		$resp = [
			'read'    => $read,
			'running' => $running,
		];

		return $resp;
	}

	/**
	 * Wrapper action callback for Read cron job.
	 *
	 * @param string $operation_url Operation URL for checking the read status.
	 * @param int    $attachment_id Attachment ID.
	 */
	public function do_read_cron( string $operation_url, int $attachment_id ) {
		$feature  = new PDFTextExtraction();
		$settings = $feature->get_settings( static::ID );

		( new Read( $settings, intval( $attachment_id ), $feature ) )->check_read_result( $operation_url );
	}

	/**
	 * Generate smart-cropped image thumbnails.
	 *
	 * @since 1.5.0
	 *
	 * @param array $metadata Attachment metadata.
	 * @param int   $attachment_id Attachment ID.
	 * @return array|WP_Error
	 */
	public function smart_crop_image( array $metadata, int $attachment_id ) {
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'invalid', esc_html__( 'This attachment can\'t be processed.', 'classifai' ) );
		}

		$feature  = new ImageCropping();
		$settings = $feature->get_settings( static::ID );

		if ( ! is_array( $metadata ) || ! is_array( $settings ) ) {
			return new WP_Error( 'invalid', esc_html__( 'Invalid data found. Please check your settings and try again.', 'classifai' ) );
		}

		$should_smart_crop = $feature->is_feature_enabled();

		/**
		 * Filters whether to apply smart cropping to the current image.
		 *
		 * @since 1.5.0
		 * @hook classifai_should_smart_crop_image
		 *
		 * @param {bool}  $should_smart_crop Whether to apply smart cropping. The default value is set in ComputerVision settings.
		 * @param {array} $metadata          Image metadata.
		 * @param {int}   $attachment_id     The attachment ID.
		 *
		 * @return {bool} Whether to apply smart cropping.
		 */
		if ( ! apply_filters( 'classifai_should_smart_crop_image', $should_smart_crop, $metadata, $attachment_id ) ) {
			return [];
		}

		// Direct file system access is required for the current implementation of this feature.
		if ( ! function_exists( 'get_filesystem_method' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$access_type = get_filesystem_method();
		if ( 'direct' !== $access_type || ! WP_Filesystem() ) {
			return new WP_Error( 'access', esc_html__( 'Access to the filesystem is required for this feature to work.', 'classifai' ) );
		}

		$smart_cropping = new SmartCropping( $settings );

		return $smart_cropping->generate_cropped_images( $metadata, intval( $attachment_id ) );
	}

	/**
	 * Runs text recognition on the attachment.
	 *
	 * @since 1.6.0
	 *
	 * @param array $metadata Attachment metadata.
	 * @param int   $attachment_id Attachment ID.
	 * @return string|WP_Error
	 */
	public function ocr_processing( array $metadata = [], int $attachment_id = 0 ) {
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'invalid', esc_html__( 'This attachment can\'t be processed.', 'classifai' ) );
		}

		$feature  = new ImageTextExtraction();
		$settings = $feature->get_settings( static::ID );

		if ( ! is_array( $metadata ) || ! is_array( $settings ) ) {
			return new WP_Error( 'invalid', esc_html__( 'Invalid data found. Please check your settings and try again.', 'classifai' ) );
		}

		$should_ocr_scan = $feature->is_feature_enabled();

		/**
		 * Filters whether to run OCR scanning on the current image.
		 *
		 * @since 1.6.0
		 * @hook classifai_should_ocr_scan_image
		 *
		 * @param {bool}  $should_ocr_scan Whether to run OCR scanning. The default value is set in ComputerVision settings.
		 * @param {array} $metadata        Image metadata.
		 * @param {int}   $attachment_id   The attachment ID.
		 *
		 * @return {bool} Whether to run OCR scanning.
		 */
		if ( ! apply_filters( 'classifai_should_ocr_scan_image', $should_ocr_scan, $metadata, $attachment_id ) ) {
			return '';
		}

		$image_url = wp_get_attachment_url( $attachment_id );
		$scan      = $this->scan_image( $image_url, $feature );

		$ocr      = new OCR( $settings, $scan );
		$response = $ocr->generate_ocr_data( $metadata, $attachment_id );

		set_transient( 'classifai_azure_computer_vision_image_text_extraction_latest_response', $scan, DAY_IN_SECONDS * 30 );

		return $response;
	}

	/**
	 * Generate alt tags for an image.
	 *
	 * @param string $image_url URL of image to process.
	 * @param int    $attachment_id Post ID for the attachment.
	 * @return string|WP_Error
	 */
	public function generate_alt_tags( string $image_url, int $attachment_id ) {
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'invalid', esc_html__( 'This attachment can\'t be processed.', 'classifai' ) );
		}

		$feature = new DescriptiveTextGenerator();
		$rtn     = '';

		$details = $this->scan_image( $image_url, $feature );

		if ( is_wp_error( $details ) ) {
			return $details;
		}

		$captions = $details->description->captions ?? [];

		set_transient( 'classifai_azure_computer_vision_descriptive_text_latest_response', $details, DAY_IN_SECONDS * 30 );

		/**
		 * Filter the captions returned from the API.
		 *
		 * @since 1.4.0
		 * @hook classifai_computer_vision_captions
		 *
		 * @param {array} $captions The returned caption data.
		 *
		 * @return {array} The filtered caption data.
		 */
		$captions = apply_filters( 'classifai_computer_vision_captions', $captions );

		// Process the returned captions to see if they pass the threshold.
		if ( is_array( $captions ) && ! empty( $captions ) ) {
			$settings  = $feature->get_settings( static::ID );
			$threshold = $settings['descriptive_confidence_threshold'];

			// Check the first caption to see if it passes the threshold.
			if ( $captions[0]->confidence * 100 > $threshold ) {
				$rtn = $captions[0]->text;
			} else {
				/**
				 * Fires if there were no captions returned.
				 *
				 * @since 1.5.0
				 * @hook classifai_computer_vision_caption_failed
				 *
				 * @param {array} $tags      The caption data.
				 * @param {int}   $threshold The caption_threshold setting.
				 */
				do_action( 'classifai_computer_vision_caption_failed', $captions, $threshold );
			}

			// Save all the results for later.
			update_post_meta( $attachment_id, 'classifai_computer_vision_captions', $captions );
		}

		return $rtn;
	}

	/**
	 * Read PDF content and update the description of attachment.
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return string|WP_Error
	 */
	public function read_pdf( int $attachment_id ) {
		$feature         = new PDFTextExtraction();
		$settings        = $feature->get_settings( static::ID );
		$should_read_pdf = $feature->is_feature_enabled();

		if ( ! $should_read_pdf ) {
			return new WP_Error( 'not_enabled', esc_html__( 'PDF Text Extraction is disabled. Please check your settings.', 'classifai' ) );
		}

		// Direct file system access is required for the current implementation of this feature.
		if ( ! function_exists( 'get_filesystem_method' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		$access_type = get_filesystem_method();

		if ( 'direct' !== $access_type || ! WP_Filesystem() ) {
			return new WP_Error( 'invalid_access_type', 'Invalid access type! Direct file system access is required.' );
		}

		$read = new Read( $settings, intval( $attachment_id ), $feature );

		return $read->read_document();
	}

	/**
	 * Generate the image tags for the passed in image.
	 *
	 * @param string $image_url URL of image to process.
	 * @param int    $attachment_id Post ID for the attachment.
	 * @return array|WP_Error
	 */
	public function generate_image_tags( string $image_url, int $attachment_id ) {
		if ( ! wp_attachment_is_image( $attachment_id ) ) {
			return new WP_Error( 'invalid', esc_html__( 'This attachment can\'t be processed.', 'classifai' ) );
		}

		$rtn      = [];
		$feature  = new ImageTagsGenerator();
		$settings = $feature->get_settings( static::ID );

		$details = $this->scan_image( $image_url, $feature );

		if ( is_wp_error( $details ) ) {
			return $details;
		}

		$tags = $details->tags ?? [];

		set_transient( 'classifai_azure_computer_vision_image_tags_latest_response', $details, DAY_IN_SECONDS * 30 );

		/**
		 * Filter the tags returned from the API.
		 *
		 * @since 1.4.0
		 * @hook classifai_computer_vision_image_tags
		 *
		 * @param {array} $tags The image tag data.
		 *
		 * @return {array} The filtered image tags.
		 */
		$tags = apply_filters( 'classifai_computer_vision_image_tags', $tags );

		// Process the returned tags to see if they pass the threshold.
		if ( is_array( $tags ) && ! empty( $tags ) ) {
			$threshold   = $settings['tag_confidence_threshold'];
			$custom_tags = [];

			// Save each tag if it passes the threshold.
			foreach ( $tags as $tag ) {
				if ( $tag->confidence * 100 > $threshold ) {
					$custom_tags[] = $tag->name;
				}
			}

			if ( ! empty( $custom_tags ) ) {
				$rtn = $custom_tags;
			} else {
				/**
				 * Fires if there were no tags added.
				 *
				 * @since 1.5.0
				 * @hook classifai_computer_vision_image_tag_failed
				 *
				 * @param {array} $tags      The image tag data.
				 * @param {int}   $threshold The tag_threshold setting.
				 */
				do_action( 'classifai_computer_vision_image_tag_failed', $tags, $threshold );
			}

			// Save all the tags for later.
			update_post_meta( $attachment_id, 'classifai_computer_vision_image_tags', $tags );
		}

		return $rtn;
	}

	/**
	 * Scan the image and return the results.
	 *
	 * @param string                      $image_url Path to the uploaded image.
	 * @param \Classifai\Features\Feature $feature   Feature instance
	 * @return bool|object|WP_Error
	 */
	protected function scan_image( string $image_url, \Classifai\Features\Feature $feature = null ) {
		$settings = $feature->get_settings( static::ID );

		// Check if valid authentication is in place.
		if ( ! $feature->is_feature_enabled() ) {
			return new WP_Error( 'feature_disabled', esc_html__( 'Feature not enabled.', 'classifai' ) );
		}

		$endpoint_url = $this->prep_api_url( $feature );

		/*
		 * Azure AI Vision requires full image URL. So, if the file URL is relative,
		 * then we transform it into a full URL.
		 */
		if ( '/' === substr( $image_url, 0, 1 ) ) {
			$image_url = get_site_url() . $image_url;
		}

		$response = wp_remote_post(
			$endpoint_url,
			[
				'headers' => [
					'Ocp-Apim-Subscription-Key' => $settings['api_key'],
					'Content-Type'              => 'application/json',
				],
				'body'    => '{"url":"' . $image_url . '"}',
			]
		);

		if ( ! is_wp_error( $response ) ) {
			$body = json_decode( wp_remote_retrieve_body( $response ) );

			if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
				if ( isset( $body->error ) ) {
					$rtn = new WP_Error( $body->error->code ?? 'error', $body->error->message ?? esc_html__( 'An error occurred.', 'classifai' ), $body );
				} elseif ( isset( $body->message ) ) {
					$rtn = new WP_Error( $body->code ?? 'error', $body->message, $body );
				} else {
					$rtn = new WP_Error( 'error', esc_html__( 'An error occurred.', 'classifai' ), $body );
				}
			} else {
				$rtn = $body;
			}
		} else {
			$rtn = $response;
		}

		return $rtn;
	}

	/**
	 * Build and return the API endpoint based on settings.
	 *
	 * @param \Classifai\Features\Feature $feature Feature instance
	 * @return string
	 */
	protected function prep_api_url( \Classifai\Features\Feature $feature = null ): string {
		$settings     = $feature->get_settings( static::ID );
		$api_features = [];

		if ( $feature instanceof DescriptiveTextGenerator && $feature->is_feature_enabled() && ! empty( $feature->get_alt_text_settings() ) ) {
			$api_features[] = 'Description';
		}

		if ( $feature instanceof ImageTagsGenerator && $feature->is_feature_enabled() ) {
			$api_features[] = 'Tags';
		}

		$endpoint = add_query_arg( 'visualFeatures', implode( ',', $api_features ), trailingslashit( $settings['endpoint_url'] ) . $this->analyze_url );

		return $endpoint;
	}

	/**
	 * Authenticates our credentials.
	 *
	 * @param string $url     Endpoint URL.
	 * @param string $api_key Api Key.
	 * @return bool|WP_Error
	 */
	protected function authenticate_credentials( string $url, string $api_key ) {
		$rtn     = false;
		$request = wp_remote_post(
			trailingslashit( $url ) . $this->analyze_url,
			[
				'headers' => [
					'Ocp-Apim-Subscription-Key' => $api_key,
					'Content-Type'              => 'application/json',
				],
				'body'    => '{"url":"https://classifaiplugin.com/wp-content/themes/classifai-theme/assets/img/header.png"}',
			]
		);

		if ( ! is_wp_error( $request ) ) {
			$response = json_decode( wp_remote_retrieve_body( $request ) );
			if ( ! empty( $response->error ) ) {
				$rtn = new WP_Error( 'auth', $response->error->message );
			} else {
				$rtn = true;
			}
		}

		return $rtn;
	}

	/**
	 * Common entry point for all REST endpoints for this provider.
	 *
	 * @param int    $attachment_id The attachment ID we're processing.
	 * @param string $route_to_call The name of the route we're going to be processing.
	 * @param array  $args          Optional arguments to pass to the route.
	 * @return array|string|WP_Error
	 */
	public function rest_endpoint_callback( $attachment_id, string $route_to_call = '', array $args = [] ) {
		// Check to be sure the post both exists and is an attachment.
		if ( ! get_post( $attachment_id ) || 'attachment' !== get_post_type( $attachment_id ) ) {
			/* translators: %1$s: the attachment ID */
			return new WP_Error( 'incorrect_ID', sprintf( esc_html__( '%1$d is not found or is not an attachment', 'classifai' ), $attachment_id ), [ 'status' => 404 ] );
		}

		if ( 'read_pdf' === $route_to_call ) {
			return $this->read_pdf( $attachment_id );
		}

		$metadata = wp_get_attachment_metadata( $attachment_id );

		if ( ! $metadata || ! is_array( $metadata ) ) {
			return new WP_Error( 'invalid', esc_html__( 'No valid metadata found.', 'classifai' ) );
		}

		switch ( $route_to_call ) {
			case 'ocr':
				return $this->ocr_processing( $metadata, $attachment_id );

			case 'crop':
				return $this->smart_crop_image( $metadata, $attachment_id );
		}

		$image_url = get_modified_image_source_url( $attachment_id );

		if ( empty( $image_url ) || ! filter_var( $image_url, FILTER_VALIDATE_URL ) ) {
			if ( isset( $metadata['sizes'] ) && is_array( $metadata['sizes'] ) ) {
				$image_url = get_largest_acceptable_image_url(
					get_attached_file( $attachment_id ),
					wp_get_attachment_url( $attachment_id ),
					$metadata['sizes'],
					computer_vision_max_filesize()
				);
			} else {
				$image_url = wp_get_attachment_url( $attachment_id );
			}
		}

		if ( empty( $image_url ) ) {
			return new WP_Error( 'error', esc_html__( 'Valid image size not found. Make sure the image is less than 4MB.' ) );
		}

		switch ( $route_to_call ) {
			case 'descriptive_text':
				return $this->generate_alt_tags( $image_url, $attachment_id );

			case 'tags':
				return $this->generate_image_tags( $image_url, $attachment_id );
		}
	}

	/**
	 * Filter the SQL clauses of an attachment query to include tags and alt text.
	 *
	 * @param array $clauses An array including WHERE, GROUP BY, JOIN, ORDER BY,
	 *                       DISTINCT, fields (SELECT), and LIMITS clauses.
	 * @return array The modified clauses.
	 */
	public function filter_attachment_query_keywords( array $clauses ): array {
		global $wpdb;
		remove_filter( 'posts_clauses', __FUNCTION__ );

		if ( ! preg_match( "/\({$wpdb->posts}.post_content (NOT LIKE|LIKE) (\'[^']+\')\)/", $clauses['where'] ) ) {
			return $clauses;
		}

		// Add a LEFT JOIN of the postmeta table so we don't trample existing JOINs.
		$clauses['join'] .= " LEFT JOIN {$wpdb->postmeta} AS classifai_postmeta ON ( {$wpdb->posts}.ID = classifai_postmeta.post_id AND ( classifai_postmeta.meta_key = 'classifai_computer_vision_image_tags' OR classifai_postmeta.meta_key = '_wp_attachment_image_alt' ) )";

		$clauses['groupby'] = "{$wpdb->posts}.ID";

		$clauses['where'] = preg_replace(
			"/\({$wpdb->posts}.post_content (NOT LIKE|LIKE) (\'[^']+\')\)/",
			'$0 OR ( classifai_postmeta.meta_value $1 $2 )',
			$clauses['where']
		);

		return $clauses;
	}

	/**
	 * Returns the debug information for the provider settings.
	 *
	 * @return array
	 */
	public function get_debug_information(): array {
		$settings          = [];
		$provider_settings = [];
		$debug_info        = [];

		if ( $this->feature_instance ) {
			$settings          = $this->feature_instance->get_settings();
			$provider_settings = $settings[ static::ID ];
		}

		if ( $this->feature_instance instanceof DescriptiveTextGenerator ) {
			if ( ! isset( $provider_settings['descriptive_text_fields'] ) || ! is_array( $provider_settings['descriptive_text_fields'] ) ) {
				$provider_settings['descriptive_text_fields'] = array(
					'alt'         => 0,
					'caption'     => 0,
					'description' => 0,
				);
			}

			$descriptive_text = array_filter(
				$provider_settings['descriptive_text_fields'],
				function ( $type ) {
					return '0' !== $type;
				}
			);

			$debug_info[ __( 'Generate descriptive text', 'classifai' ) ] = implode( ', ', $descriptive_text );
			$debug_info[ __( 'Confidence threshold', 'classifai' ) ]      = $provider_settings['descriptive_confidence_threshold'];
			$debug_info[ __( 'Latest response:', 'classifai' ) ]          = $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_descriptive_text_latest_response' ) );
		}

		if ( $this->feature_instance instanceof ImageTagsGenerator ) {
			$debug_info[ __( 'Tag taxonomy', 'classifai' ) ]         = $provider_settings['tag_taxonomy'] ?? 'image_tags';
			$debug_info[ __( 'Confidence threshold', 'classifai' ) ] = $provider_settings['tag_confidence_threshold'];
			$debug_info[ __( 'Latest response:', 'classifai' ) ]     = $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_image_tags_latest_response' ) );
		}

		if ( $this->feature_instance instanceof ImageCropping ) {
			$debug_info[ __( 'Latest response:', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_image_cropping_latest_response' ) );
		}

		if ( $this->feature_instance instanceof ImageTextExtraction ) {
			$debug_info[ __( 'Latest response:', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_image_text_extraction_latest_response' ) );
		}

		if ( $this->feature_instance instanceof PDFTextExtraction ) {
			$debug_info[ __( 'Latest response:', 'classifai' ) ] = $this->get_formatted_latest_response( get_transient( 'classifai_azure_computer_vision_pdf_text_extraction_check_result_latest_response' ) );
		}

		return apply_filters(
			'classifai_' . self::ID . '_debug_information',
			$debug_info,
			$settings,
		);
	}
}