utility.inc 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <?php
  2. /**
  3. * @file utility.inc: uitility form, conversion and rendering functions for
  4. * image processing.
  5. */
  6. /**
  7. * Validates that the element is a positive number.
  8. *
  9. * This is a Form API #element_validate callback.
  10. *
  11. * @param array $element
  12. * param array $form_status
  13. */
  14. function imagecache_actions_validate_number_positive(&$element/*, &$form_status*/) {
  15. $value = $element['#value'];
  16. if ($value != '') {
  17. if (!is_numeric($value) || (float) $value <= 0.0) {
  18. form_error($element, t('%name must be a positive number.', array('%name' => $element['#title'])));
  19. }
  20. }
  21. }
  22. /**
  23. * Validates that the element is a non negative number.
  24. *
  25. * This is a Form API #element_validate callback.
  26. *
  27. * @param array $element
  28. * param array $form_status
  29. */
  30. function imagecache_actions_validate_number_non_negative(&$element/*, &$form_status*/) {
  31. $value = $element['#value'];
  32. if ($value != '') {
  33. if (!is_numeric($value) || (float) $value < 0.0) {
  34. form_error($element, t('%name must be a non negative number.', array('%name' => $element['#title'])));
  35. }
  36. }
  37. }
  38. /*
  39. * File field handling.
  40. */
  41. /**
  42. * @return string
  43. */
  44. function imagecache_actions_file_field_description() {
  45. return t('File is either a file with one of the valid schemes, an absolute path, or a relative path (relative to the current directory, probably the Drupal site root). Valid schemes are a.o. private://, public://, and if the !module module has been installed, also module:// and theme://.',
  46. array('!module' => l('System Stream Wrapper', 'https://drupal.org/project/system_stream_wrapper', array('external' => TRUE))));
  47. }
  48. /**
  49. * Validates that the file as specified in the element exists and is readable.
  50. *
  51. * This is a Form API #element_validate callback.
  52. *
  53. * @param array $element
  54. * param array $form_status
  55. */
  56. function imagecache_actions_validate_file(&$element/*, &$form_status*/) {
  57. if (!imagecache_actions_find_file($element['#value'])) {
  58. form_error($element, t("Unable to find the file '%file'. Please check the path.", array('%file' => $element['#value'])));
  59. }
  60. }
  61. /**
  62. * Looks up and returns the full path of the given file.
  63. *
  64. * @param string $file
  65. * A file name. We accept:
  66. * - stream wrapper notation like: private://, public://, temporary:// and
  67. * module:// (the latter provided by the system stream wrapper module).
  68. * - relative: relative to the current directory (probably DRUPAL_ROOT).
  69. * - absolute: as is.
  70. *
  71. * @return string|false
  72. * The full file path of the file, so the image toolkit knows exactly where it
  73. * is. False if the file cannot be found or is not readable.
  74. */
  75. function imagecache_actions_find_file($file) {
  76. $result = FALSE;
  77. if (is_readable($file)) {
  78. $result = drupal_realpath($file);
  79. }
  80. return $result;
  81. }
  82. /**
  83. * Loads the given file as an image object.
  84. *
  85. * @param string $file
  86. * A file name, with a scheme, relative, or absolute.
  87. * @param bool $toolkit
  88. *
  89. * @return object|FALSE
  90. * The image object.
  91. *
  92. * @see imagecache_actions_find_file()
  93. * @see image_load()
  94. */
  95. function imagecache_actions_image_load($file, $toolkit = FALSE) {
  96. $full_path = imagecache_actions_find_file($file);
  97. if (!$full_path) {
  98. trigger_error("Failed to find file $file.", E_USER_ERROR);
  99. return FALSE;
  100. }
  101. $image = image_load($full_path, $toolkit);
  102. if (!$image) {
  103. trigger_error("Failed to open file '$file' as an image resource.", E_USER_ERROR);
  104. }
  105. return $image;
  106. }
  107. /**
  108. * Returns the label for the given style name.
  109. *
  110. * As of D7.23 image styles can also have a human readable label besides their
  111. * machine readable name. This function returns that label if available, or the
  112. * name if the label is absent.
  113. *
  114. * @param string|null $style_name
  115. * The name of the image style.
  116. *
  117. * @return string
  118. * The label for the image style.
  119. */
  120. function imagecache_actions_get_style_label($style_name) {
  121. if (!empty($style_name)) {
  122. $style = image_style_load($style_name);
  123. $label = isset($style['label']) ? $style['label'] : $style['name'];
  124. if (!empty($style)) {
  125. return $label;
  126. }
  127. else {
  128. return $label . ' ' . t('<span class="error">Invalid reference. The referenced image style may have been deleted!</span>');
  129. }
  130. }
  131. else {
  132. return t('none');
  133. }
  134. }
  135. /**
  136. * Return an array with context information about the image.
  137. *
  138. * This information is called by effects that need contextual information or
  139. * that allow custom PHP:
  140. * - Custom action.
  141. * - Text from image alt or title.
  142. * - Text with tokens.
  143. * - Text from PHP code.
  144. *
  145. * @param object $image
  146. * The image object.
  147. * @param array $data
  148. * An associative array with the effect options.
  149. *
  150. * @return array
  151. * An associative array with context information about the image. It contains
  152. * the following keys:
  153. * - effect_data: array with the effect data.
  154. * - managed_file: object|null, a managed file object for the current image.
  155. * This may be (an extended) media/file_entity file object.
  156. * - referring_entities: array, list of (loaded) entities that have an image
  157. * field referring to the managed file.
  158. * - entity: object|null, the 1st (and often only) entity that has an image
  159. * field referring to the managed file.
  160. * - image_field: array|null, the (1st and often only) image field item of
  161. * entity that refers to the managed file (single field item, not the
  162. * whole field value).
  163. */
  164. function imagecache_actions_get_image_context($image, $data) {
  165. // Store context about the image.
  166. $image_context = array(
  167. 'effect_data' => $data,
  168. 'managed_file' => NULL,
  169. 'referring_entities' => array(),
  170. 'entity' => NULL,
  171. 'image_field' => NULL,
  172. );
  173. // Find the managed file object (at most 1 object as 'uri' is a unique index).
  174. $managed_file = file_load_multiple(array(), array('uri' => $image->source));
  175. $managed_file = reset($managed_file);
  176. if ($managed_file !== FALSE) {
  177. $image_context['managed_file'] = $managed_file;
  178. // And find the entities referring to this managed file, image fields first,
  179. // then file fields (very specific use cases).
  180. $references = file_get_file_references($managed_file, NULL, FIELD_LOAD_CURRENT, 'image')
  181. + file_get_file_references($managed_file, NULL, FIELD_LOAD_CURRENT, 'file');
  182. if ($references) {
  183. // Load referring entities.
  184. foreach ($references as $field_name => $field_references) {
  185. foreach ($field_references as $entity_type => $entity_stubs) {
  186. $image_context['referring_entities'][$field_name][$entity_type] = entity_load($entity_type, array_keys($entity_stubs));
  187. }
  188. }
  189. // Make it easy to access the '1st' entity and its referring image field.
  190. reset($image_context['referring_entities']);
  191. list($field_name, $field_references) = each($image_context['referring_entities']);
  192. reset($field_references);
  193. list($entity_type, $entities) = each($field_references);
  194. reset($entities);
  195. list(, $image_context['entity']) = each($entities);
  196. /** @var array|false $image_field */
  197. $image_field = field_get_items($entity_type, $image_context['entity'], $field_name);
  198. if ($image_field) {
  199. // Get referring item.
  200. foreach ($image_field as $image_field_value) {
  201. if ($image_field_value['fid'] === $managed_file->fid) {
  202. // @todo: file_field as well or just ignore the type of field?
  203. $image_context['image_field'] = $image_field_value;
  204. break;
  205. }
  206. }
  207. }
  208. }
  209. }
  210. return $image_context;
  211. }
  212. /**
  213. * Given two imageapi objects with dimensions, and some positioning values,
  214. * calculate a new x,y for the layer to be placed at.
  215. *
  216. * This is a different approach to imagecache_actions_keyword_filter() - more
  217. * like css.
  218. *
  219. * The $style is an array, and expected to have 'top, bottom, left, right'
  220. * attributes set. These values may be positive, negative, or in %.
  221. *
  222. * % is calculated relative to the base image dimensions.
  223. * Using % requires that the layer is positioned CENTERED on that location, so
  224. * some offsets are added to it. 'right-25%' is not lined up with a margin 25%
  225. * in, it's centered at a point 25% in - which is therefore identical with
  226. * left+75%
  227. *
  228. * @param $base
  229. * @param $layer
  230. * @param $style
  231. *
  232. * @return array
  233. * A keyed array of absolute x,y co-ordinates to place the layer at.
  234. */
  235. function imagecache_actions_calculate_relative_position($base, $layer, $style) {
  236. // Both images should now have size info available.
  237. if (isset($style['bottom'])) {
  238. $ypos = imagecache_actions_calculate_offset('bottom', $style['bottom'], $base->info['height'], $layer->info['height']);
  239. }
  240. if (isset($style['top'])) {
  241. $ypos = imagecache_actions_calculate_offset('top', $style['top'], $base->info['height'], $layer->info['height']);
  242. }
  243. if (isset($style['right'])) {
  244. $xpos = imagecache_actions_calculate_offset('right', $style['right'], $base->info['width'], $layer->info['width']);
  245. }
  246. if (isset($style['left'])) {
  247. $xpos = imagecache_actions_calculate_offset('left', $style['left'], $base->info['width'], $layer->info['width']);
  248. }
  249. if (!isset($ypos)) {
  250. // Assume center.
  251. $ypos = ($base->info['height'] / 2) - ($layer->info['height'] / 2);
  252. }
  253. if (!isset($xpos)) {
  254. // Assume center.
  255. $xpos = ($base->info['width'] / 2) - ($layer->info['width'] / 2);
  256. }
  257. // dpm(__FUNCTION__ . " Calculated offsets");
  258. // dpm(get_defined_vars());
  259. return array('x' => $xpos, 'y' => $ypos);
  260. }
  261. /**
  262. * Calculates an offset from an edge.
  263. *
  264. * Positive numbers are IN from the edge, negative offsets are OUT.
  265. *
  266. * Examples:
  267. * - left, 20, 200, 100 = 20
  268. * - right, 20, 200, 100 = 80 (object 100 wide placed 20px from the right)
  269. * - top, 50%, 200, 100 = 50 (Object is centered when using %)
  270. * - top, 20%, 200, 100 = -10
  271. * - bottom, -20, 200, 100 = 220
  272. * - right, -25%, 200, 100 = 200 (this ends up just off screen)
  273. *
  274. * Also, the value can be a string, eg "bottom-100", or "center+25%"
  275. * @param string $keyword
  276. * The edge to calculate the offset from. Can be one of: left, right, top,
  277. * bottom, middle, center.
  278. * @param string $value
  279. * The value to offset with: can be:
  280. * - a numeric value: offset in pixels
  281. * - a percentage value: offset with a percentage of the $base_size.
  282. * - a keyword indicating a pxiel size: left, right, top, bottom, middle,
  283. * center.
  284. * - an expression of the format: keyword +|- value[%], e.g. center+25%.
  285. * @param int $base_size
  286. * The size of the dimension. Used to calculate percentages of.
  287. * @param int $layer_size
  288. * The size of the canvas in the current dimension. The value to take for
  289. * $keyword will be based on this value.
  290. *
  291. * @return int
  292. */
  293. function imagecache_actions_calculate_offset($keyword, $value, $base_size, $layer_size) {
  294. $offset = 0;
  295. // Used to account for dimensions of the placed object.
  296. $direction = 1;
  297. $base = 0;
  298. if ($keyword == 'right' || $keyword == 'bottom') {
  299. $direction = -1;
  300. $offset = -1 * $layer_size;
  301. $base = $base_size;
  302. }
  303. if ($keyword == 'middle' || $keyword == 'center') {
  304. $base = $base_size / 2;
  305. $offset = -1 * ($layer_size / 2);
  306. }
  307. // Keywords may be used to stand in for numeric values.
  308. switch ($value) {
  309. case 'left':
  310. case 'top':
  311. $value = 0;
  312. break;
  313. case 'middle':
  314. case 'center':
  315. $value = $base_size / 2;
  316. break;
  317. case 'bottom':
  318. case 'right':
  319. $value = $base_size;
  320. }
  321. // Handle keyword-number cases like top+50% or bottom-100px,
  322. // @see imagecache_actions_keyword_filter().
  323. if (preg_match('/^([a-z]+) *([+-]) *(\d+)((px|%)?)$/', $value, $results)) {
  324. list(, $value_key, $value_mod, $mod_value, $mod_unit) = $results;
  325. if ($mod_unit == '%') {
  326. $mod_value = $mod_value / 100 * $base_size;
  327. }
  328. $mod_direction = ($value_mod == '-') ? -1 : +1;
  329. switch ($value_key) {
  330. case 'left':
  331. case 'top':
  332. default:
  333. $mod_base = 0;
  334. break;
  335. case 'middle':
  336. case 'center':
  337. $mod_base = $base_size / 2;
  338. break;
  339. case 'bottom':
  340. case 'right':
  341. $mod_base = $base_size;
  342. break;
  343. }
  344. $modified_value = $mod_base + ($mod_direction * $mod_value);
  345. return $modified_value;
  346. }
  347. // Handle % values.
  348. if (substr($value, strlen($value) - 1, 1) == '%') {
  349. $value = intval($value / 100 * $base_size);
  350. $offset = -1 * ($layer_size / 2);
  351. }
  352. $value = $base + ($direction * $value);
  353. // Add any extra offset to position the item.
  354. return $value + $offset;
  355. }
  356. /**
  357. * Convert a hex string to its RGBA (Red, Green, Blue, Alpha) integer
  358. * components.
  359. *
  360. * Stolen from imageapi D6 2011-01
  361. *
  362. * @param string $hex
  363. * A string specifing an RGB color in the formats:
  364. * '#ABC','ABC','#ABCD','ABCD','#AABBCC','AABBCC','#AABBCCDD','AABBCCDD'
  365. *
  366. * @return array
  367. * An array with four elements for red, green, blue, and alpha.
  368. */
  369. function imagecache_actions_hex2rgba($hex) {
  370. $hex = ltrim($hex, '#');
  371. if (preg_match('/^[0-9a-f]{3}$/i', $hex)) {
  372. // 'FA3' is the same as 'FFAA33' so r=FF, g=AA, b=33
  373. $r = str_repeat($hex{0}, 2);
  374. $g = str_repeat($hex{1}, 2);
  375. $b = str_repeat($hex{2}, 2);
  376. $a = '0';
  377. }
  378. elseif (preg_match('/^[0-9a-f]{6}$/i', $hex)) {
  379. // #FFAA33 or r=FF, g=AA, b=33
  380. list($r, $g, $b) = str_split($hex, 2);
  381. $a = '0';
  382. }
  383. elseif (preg_match('/^[0-9a-f]{8}$/i', $hex)) {
  384. // #FFAA33 or r=FF, g=AA, b=33
  385. list($r, $g, $b, $a) = str_split($hex, 2);
  386. }
  387. elseif (preg_match('/^[0-9a-f]{4}$/i', $hex)) {
  388. // 'FA37' is the same as 'FFAA3377' so r=FF, g=AA, b=33, a=77
  389. $r = str_repeat($hex{0}, 2);
  390. $g = str_repeat($hex{1}, 2);
  391. $b = str_repeat($hex{2}, 2);
  392. $a = str_repeat($hex{3}, 2);
  393. }
  394. else {
  395. // error: invalid hex string, @todo: set form error.
  396. return FALSE;
  397. }
  398. $r = hexdec($r);
  399. $g = hexdec($g);
  400. $b = hexdec($b);
  401. $a = hexdec($a);
  402. // Alpha over 127 is illegal. assume they meant half that.
  403. if ($a > 127) {
  404. $a = (int) $a / 2;
  405. }
  406. return array('red' => $r, 'green' => $g, 'blue' => $b, 'alpha' => $a);
  407. }
  408. /**
  409. * Accept a keyword (center, top, left, etc) and return it as an offset in pixels.
  410. * Called on either the x or y values.
  411. *
  412. * May be something like "20", "center", "left+20", "bottom+10". + values are
  413. * in from the sides, so bottom+10 is 10 UP from the bottom.
  414. *
  415. * "center+50" is also OK.
  416. *
  417. * "30%" will place the CENTER of the object at 30% across. to get a 30% margin,
  418. * use "left+30%"
  419. *
  420. * @param string|int $value
  421. * string or int value.
  422. * @param int $base_size
  423. * Size in pixels of the range this item is to be placed in.
  424. * @param int $layer_size
  425. * Size in pixels of the object to be placed.
  426. *
  427. * @return int
  428. */
  429. function imagecache_actions_keyword_filter($value, $base_size, $layer_size) {
  430. // See above for the patterns this matches.
  431. if (!preg_match('/([a-z]*) *([+-]?) *(\d*)((px|%)?)/', $value, $results)) {
  432. trigger_error("imagecache_actions had difficulty parsing the string '$value' when calculating position. Please check the syntax.", E_USER_WARNING);
  433. }
  434. list(, $keyword, $plusminus, $value, $unit) = $results;
  435. return imagecache_actions_calculate_offset($keyword, $plusminus . $value . $unit, $base_size, $layer_size);
  436. }
  437. /**
  438. * Computes a length based on a length specification and an actual length.
  439. *
  440. * Examples:
  441. * (50, 400) returns 50; (50%, 400) returns 200;
  442. * (50, null) returns 50; (50%, null) returns null;
  443. * (null, null) returns null; (null, 100) returns null.
  444. *
  445. * @param string|null $length_specification
  446. * The length specification. An integer constant or a % specification.
  447. * @param int|null $current_length
  448. * The current length. May be null.
  449. *
  450. * @return int|null
  451. */
  452. function imagecache_actions_percent_filter($length_specification, $current_length) {
  453. if (strpos($length_specification, '%') !== FALSE) {
  454. $length_specification = $current_length !== NULL ? str_replace('%', '', $length_specification) * 0.01 * $current_length : NULL;
  455. }
  456. return $length_specification;
  457. }