class-wpseo-image-utils.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. <?php
  2. /**
  3. * WPSEO plugin file.
  4. *
  5. * @package WPSEO
  6. */
  7. /**
  8. * WPSEO_Image_Utils
  9. */
  10. class WPSEO_Image_Utils {
  11. /**
  12. * Find an attachment ID for a given URL.
  13. *
  14. * @param string $url The URL to find the attachment for.
  15. *
  16. * @return int The found attachment ID, or 0 if none was found.
  17. */
  18. public static function get_attachment_by_url( $url ) {
  19. // Because get_attachment_by_url won't work on resized versions of images, we strip out the size part of an image URL.
  20. $url = preg_replace( '/(.*)-\d+x\d+\.(jpg|png|gif)$/', '$1.$2', $url );
  21. if ( function_exists( 'wpcom_vip_attachment_url_to_postid' ) ) {
  22. // @codeCoverageIgnoreStart -- we can't test this properly.
  23. return (int) wpcom_vip_attachment_url_to_postid( $url );
  24. // @codeCoverageIgnoreEnd -- The rest we _can_ test.
  25. }
  26. return self::attachment_url_to_postid( $url );
  27. }
  28. /**
  29. * Implements the attachment_url_to_postid with use of WP Cache.
  30. *
  31. * @param string $url The attachment URL for which we want to know the Post ID.
  32. *
  33. * @return int The Post ID belonging to the attachment, 0 if not found.
  34. */
  35. protected static function attachment_url_to_postid( $url ) {
  36. $cache_key = sprintf( 'yoast_attachment_url_post_id_%s', md5( $url ) );
  37. // Set the ID based on the hashed url in the cache.
  38. $id = wp_cache_get( $cache_key );
  39. if ( $id === 'not_found' ) {
  40. return 0;
  41. }
  42. // ID is found in cache, return.
  43. if ( $id !== false ) {
  44. return $id;
  45. }
  46. // phpcs:ignore WordPress.VIP.RestrictedFunctions -- We use the WP COM version if we can, see above.
  47. $id = attachment_url_to_postid( $url );
  48. if ( empty( $id ) ) {
  49. wp_cache_set( $cache_key, 'not_found', '', ( 12 * HOUR_IN_SECONDS + mt_rand( 0, ( 4 * HOUR_IN_SECONDS ) ) ) );
  50. return 0;
  51. }
  52. // We have the Post ID, but it's not in the cache yet. We do that here and return.
  53. wp_cache_set( $cache_key, $id, '', ( 24 * HOUR_IN_SECONDS + mt_rand( 0, ( 12 * HOUR_IN_SECONDS ) ) ) );
  54. return $id;
  55. }
  56. /**
  57. * Retrieves the image data.
  58. *
  59. * @param array $image Image array with URL and metadata.
  60. * @param int $attachment_id Attachment ID.
  61. *
  62. * @return false|array $image {
  63. * Array of image data
  64. *
  65. * @type string $alt Image's alt text.
  66. * @type string $alt Image's alt text.
  67. * @type int $width Width of image.
  68. * @type int $height Height of image.
  69. * @type string $type Image's MIME type.
  70. * @type string $url Image's URL.
  71. * }
  72. */
  73. public static function get_data( $image, $attachment_id ) {
  74. if ( ! is_array( $image ) ) {
  75. return false;
  76. }
  77. // Deals with non-set keys and values being null or false.
  78. if ( empty( $image['width'] ) || empty( $image['height'] ) ) {
  79. return false;
  80. }
  81. $image['id'] = $attachment_id;
  82. $image['alt'] = self::get_alt_tag( $attachment_id );
  83. $image['pixels'] = ( (int) $image['width'] * (int) $image['height'] );
  84. if ( ! isset( $image['type'] ) ) {
  85. $image['type'] = get_post_mime_type( $attachment_id );
  86. }
  87. // Keep only the keys we need, and nothing else.
  88. return array_intersect_key( $image, array_flip( array( 'id', 'alt', 'path', 'width', 'height', 'pixels', 'type', 'size', 'url' ) ) );
  89. }
  90. /**
  91. * Checks a size version of an image to see if it's not too heavy.
  92. *
  93. * @param array $image Image to check the file size of.
  94. *
  95. * @return bool True when the image is within limits, false if not.
  96. */
  97. public static function has_usable_file_size( $image ) {
  98. if ( ! is_array( $image ) || $image === array() ) {
  99. return false;
  100. }
  101. /**
  102. * Filter: 'wpseo_image_image_weight_limit' - Determines what the maximum weight (in bytes) of an image is allowed to be, default is 2 MB.
  103. *
  104. * @api int - The maximum weight (in bytes) of an image.
  105. */
  106. $max_size = apply_filters( 'wpseo_image_image_weight_limit', 2097152 );
  107. // We cannot check without a path, so assume it's fine.
  108. if ( ! isset( $image['path'] ) ) {
  109. return true;
  110. }
  111. return ( self::get_file_size( $image ) <= $max_size );
  112. }
  113. /**
  114. * Find the right version of an image based on size.
  115. *
  116. * @param int $attachment_id Attachment ID.
  117. * @param string $size Size name.
  118. *
  119. * @return array|false Returns an array with image data on success, false on failure.
  120. */
  121. public static function get_image( $attachment_id, $size ) {
  122. $image = false;
  123. if ( $size === 'full' ) {
  124. $image = self::get_full_size_image_data( $attachment_id );
  125. }
  126. if ( ! $image ) {
  127. $image = image_get_intermediate_size( $attachment_id, $size );
  128. $image['size'] = $size;
  129. }
  130. if ( ! $image ) {
  131. return false;
  132. }
  133. return self::get_data( $image, $attachment_id );
  134. }
  135. /**
  136. * Returns the image data for the full size image.
  137. *
  138. * @param int $attachment_id Attachment ID.
  139. *
  140. * @return array|false Array when there is a full size image. False if not.
  141. */
  142. protected static function get_full_size_image_data( $attachment_id ) {
  143. $image = wp_get_attachment_metadata( $attachment_id );
  144. if ( ! is_array( $image ) ) {
  145. return false;
  146. }
  147. $image['url'] = wp_get_attachment_image_url( $attachment_id, 'full' );
  148. $image['path'] = get_attached_file( $attachment_id );
  149. $image['size'] = 'full';
  150. return $image;
  151. }
  152. /**
  153. * Finds the full file path for a given image file.
  154. *
  155. * @param string $path The relative file path.
  156. *
  157. * @return string The full file path.
  158. */
  159. public static function get_absolute_path( $path ) {
  160. static $uploads;
  161. if ( $uploads === null ) {
  162. $uploads = wp_get_upload_dir();
  163. }
  164. // Add the uploads basedir if the path does not start with it.
  165. if ( empty( $uploads['error'] ) && strpos( $path, $uploads['basedir'] . DIRECTORY_SEPARATOR ) !== 0 ) {
  166. return $uploads['basedir'] . DIRECTORY_SEPARATOR . ltrim( $path, DIRECTORY_SEPARATOR );
  167. }
  168. return $path;
  169. }
  170. /**
  171. * Get the relative path of the image.
  172. *
  173. * @param string $img Image URL.
  174. *
  175. * @return string The expanded image URL.
  176. */
  177. public static function get_relative_path( $img ) {
  178. if ( $img[0] !== '/' ) {
  179. return $img;
  180. }
  181. // If it's a relative URL, it's relative to the domain, not necessarily to the WordPress install, we
  182. // want to preserve domain name and URL scheme (http / https) though.
  183. $parsed_url = wp_parse_url( home_url() );
  184. $img = $parsed_url['scheme'] . '://' . $parsed_url['host'] . $img;
  185. return $img;
  186. }
  187. /**
  188. * Get the image file size.
  189. *
  190. * @param array $image An image array object.
  191. *
  192. * @return int The file size in bytes.
  193. */
  194. public static function get_file_size( $image ) {
  195. if ( isset( $image['filesize'] ) ) {
  196. return $image['filesize'];
  197. }
  198. // If the file size for the file is over our limit, we're going to go for a smaller version.
  199. // @todo save the filesize to the image metadata.
  200. // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged -- If file size doesn't properly return, we'll not fail.
  201. return @filesize( self::get_absolute_path( $image['path'] ) );
  202. }
  203. /**
  204. * Returns the different image variations for consideration.
  205. *
  206. * @param int $attachment_id The attachment to return the variations for.
  207. *
  208. * @return array The different variations possible for this attachment ID.
  209. */
  210. public static function get_variations( $attachment_id ) {
  211. $variations = array();
  212. foreach ( self::get_sizes() as $size ) {
  213. $variation = self::get_image( $attachment_id, $size );
  214. // The get_image function returns false if the size doesn't exist for this attachment.
  215. if ( $variation ) {
  216. $variations[] = $variation;
  217. }
  218. }
  219. return $variations;
  220. }
  221. /**
  222. * Check original size of image. If original image is too small, return false, else return true.
  223. *
  224. * Filters a list of variations by a certain set of usable dimensions
  225. *
  226. * @param array $usable_dimensions {
  227. * The parameters to check against.
  228. *
  229. * @type int $min_width Minimum width of image.
  230. * @type int $max_width Maximum width of image.
  231. * @type int $min_height Minimum height of image.
  232. * @type int $max_height Maximum height of image.
  233. * }
  234. * @param array $variations The variations that should be considered.
  235. *
  236. * @return array Whether a variation is fit for display or not.
  237. */
  238. public static function filter_usable_dimensions( $usable_dimensions, $variations ) {
  239. $filtered = array();
  240. foreach ( $variations as $variation ) {
  241. $dimensions = $variation;
  242. if ( self::has_usable_dimensions( $dimensions, $usable_dimensions ) ) {
  243. $filtered[] = $variation;
  244. }
  245. }
  246. return $filtered;
  247. }
  248. /**
  249. * Filters a list of variations by (disk) file size.
  250. *
  251. * @param array $variations The variations to consider.
  252. *
  253. * @return array The validations that pass the required file size limits.
  254. */
  255. public static function filter_usable_file_size( $variations ) {
  256. foreach ( $variations as $variation ) {
  257. // We return early to prevent measuring the file size of all the variations.
  258. if ( self::has_usable_file_size( $variation ) ) {
  259. return array( $variation );
  260. }
  261. }
  262. return array();
  263. }
  264. /**
  265. * Retrieve the internal WP image file sizes.
  266. *
  267. * @return array $image_sizes An array of image sizes.
  268. */
  269. public static function get_sizes() {
  270. /**
  271. * Filter: 'wpseo_image_sizes' - Determines which image sizes we'll loop through to get an appropriate image.
  272. *
  273. * @api array - The array of image sizes to loop through.
  274. */
  275. return apply_filters( 'wpseo_image_sizes', array( 'full', 'large', 'medium_large' ) );
  276. }
  277. /**
  278. * Grabs an image alt text.
  279. *
  280. * @param int $attachment_id The attachment ID.
  281. *
  282. * @return string The image alt text.
  283. */
  284. public static function get_alt_tag( $attachment_id ) {
  285. return (string) get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
  286. }
  287. /**
  288. * Checks whether an img sizes up to the parameters.
  289. *
  290. * @param array $dimensions The image values.
  291. * @param array $usable_dimensions The parameters to check against.
  292. *
  293. * @return bool True if the image has usable measurements, false if not.
  294. */
  295. private static function has_usable_dimensions( $dimensions, $usable_dimensions ) {
  296. foreach ( array( 'width', 'height' ) as $param ) {
  297. $minimum = $usable_dimensions[ 'min_' . $param ];
  298. $maximum = $usable_dimensions[ 'max_' . $param ];
  299. $current = $dimensions[ $param ];
  300. if ( ( $current < $minimum ) || ( $current > $maximum ) ) {
  301. return false;
  302. }
  303. }
  304. return true;
  305. }
  306. }