imagecache_testsuite.module 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  1. <?php
  2. /**
  3. * @file An admin-only utility to demo and check a number of imagecache presets
  4. * and actions.
  5. *
  6. * It provides a tab in Home > Administer > Site Building > Imagecache
  7. *
  8. * that lists a whole bunch of sample presets.
  9. *
  10. * @author dman http://coders.co.nz/
  11. *
  12. *
  13. */
  14. include_once('imagecache_testsuite.features.inc');
  15. /**
  16. * Implementation of hook_menu().
  17. */
  18. function imagecache_testsuite_menu() {
  19. $items = array();
  20. $items['admin/config/media/image-styles/testsuite'] = array(
  21. 'title' => 'Test Suite',
  22. 'page callback' => 'imagecache_testsuite_generate',
  23. 'access arguments' => array('administer imagecache'),
  24. 'type' => MENU_LOCAL_TASK,
  25. 'weight' => 10,
  26. );
  27. $items['admin/config/media/image-styles/testsuite/%/%'] = array(
  28. 'title' => 'Test Suite Image',
  29. 'page callback' => 'imagecache_testsuite_generate',
  30. 'page arguments' => array(5, 6),
  31. 'access arguments' => array('administer imagecache'),
  32. 'type' => MENU_CALLBACK,
  33. );
  34. $items['admin/config/media/image-styles/testsuite/positioning_test'] = array(
  35. 'title' => 'Positioning Test',
  36. 'page callback' => 'imagecache_testsuite_positioning',
  37. 'access arguments' => array('administer imagecache'),
  38. 'type' => MENU_LOCAL_TASK,
  39. );
  40. return $items;
  41. }
  42. /**
  43. * Implementation of hook_help()
  44. */
  45. function imagecache_testsuite_help($path, $arg) {
  46. switch ($path) {
  47. case 'admin/build/imagecache/test' :
  48. $output = file_get_contents(drupal_get_path('module', 'imagecache_testsuite') ."/README.txt");
  49. return _filter_autop($output);
  50. break;
  51. case 'admin/build/imagecache/test' :
  52. return t('This displays a number of examples of keyword positioning. This positioning algorithm is used when placing image overlays, such as watermarks or text on a base image canvas. Illustrated are both the expected result and the actual result. This page is just for debugging to confirm that this behavior doesnt change as the code gets updated. If the two illustrations do not match, there is probably something wrong with the calculation logic.');
  53. break;
  54. }
  55. }
  56. /**
  57. * Either returns the whole testsuite page or generates the requested
  58. * image+preset
  59. *
  60. * Flushes the entire test cache every time anything is done.
  61. */
  62. function imagecache_testsuite_generate($test_id = '', $toolkit = 'gd') {
  63. // Samples to test are scanned from
  64. // - the existing installed presets
  65. // - features inc attached to this module
  66. // - individual *.imagecache_preset.inc files found near any known modules
  67. // Images illustrating the named preset are looked for also.
  68. module_load_include('inc', 'image', 'image.admin');
  69. module_load_include('inc', 'image', 'image.effects');
  70. $sample_path = drupal_get_path('module' , 'imagecache_testsuite') ;
  71. $target = $sample_path .'/sample.jpg';
  72. $tests = image_styles() + imagecache_testsuite_get_tests();
  73. $toolkits = module_invoke_all('image_toolkits');
  74. if (empty($test_id)) {
  75. // Present the all-in-one overview page
  76. $sample_folders = imagecache_testsuite_get_folders();
  77. // Firstly, remove any previous images
  78. image_style_flush('testsuite');
  79. // Draw the admin table
  80. $test_table = array();
  81. foreach ($tests as $style_name => $style) {
  82. $row = array();
  83. $row_class = 'test';
  84. $details = '';
  85. // Render the details
  86. foreach ($style['effects'] as $i => $effect) {
  87. if (! isset($effect['name'])) {
  88. // badness
  89. watchdog('imagecache_testsuite', 'invalid testcase within %style_name. No effect name', array('%style_name' => $style_name), WATCHDOG_ERROR);
  90. $details .= '<div>Unidentified effect</div>';
  91. continue;
  92. }
  93. #$effect_definition = image_effect_definition_load($effect['name'], $style['name']);
  94. $effect_definition = image_effect_definition_load($effect['name']);
  95. if (function_exists($effect_definition['effect callback'])) {
  96. $description = "<strong>{$effect_definition['label']}</strong> ";
  97. $description .= isset($effect_definition['summary theme']) ? theme($effect_definition['summary theme'], array('data' => $effect['data'])) : '';
  98. $details .= "<div>$description</div>";
  99. }
  100. else {
  101. // Probably an action that requires a module that is not installed.
  102. $details .= t("<div><b>Action %action Unavailable</b></div>", array('%action' => $effect['name']));
  103. $row_class = 'error';
  104. }
  105. }
  106. $row['details'] = "<h3>{$style['name']}</h3><p>$details</p>";
  107. // Look for a sample image. May also be defined by the definition itself,
  108. // but normally assume a file named after the presetname, in the preset file path.
  109. foreach ($sample_folders as $sample_folder) {
  110. if (file_exists("{$sample_folder}/{$style_name}.png")) {
  111. $style['sample'] = "{$sample_folder}/{$style_name}.png";
  112. }
  113. elseif (file_exists("{$sample_folder}/{$style_name}.jpg")) {
  114. $style['sample'] = "{$sample_folder}/{$style_name}.jpg";
  115. }
  116. }
  117. if (isset($style['sample']) && file_exists($style['sample']) ) {
  118. $sample_img = theme('image', array('path' => $style['sample']));
  119. // I was having trouble with permissions on an OSX dev machine
  120. if (! is_readable($style['sample'])) {
  121. $sample_img = "FILE UNREADABLE: {$style['sample']}";
  122. }
  123. }
  124. else {
  125. $sample_img = "[no sample]";
  126. }
  127. $row['sample'] = $sample_img;
  128. // Generate a result for each available toolkit
  129. foreach ($toolkits as $toolkit => $toolkit_info) {
  130. $test_url = "admin/config/media/image-styles/testsuite/$style_name/$toolkit";
  131. $test_img = theme('image', array(
  132. 'path' => $test_url,
  133. 'alt' => "$style_name/$toolkit"
  134. ));
  135. $row[$toolkit] = l($test_img, $test_url, array('html' => TRUE));
  136. }
  137. $test_table[$style_name] = array('data' => $row, 'class' => array($row_class));
  138. }
  139. $header = array_merge(array('test', 'sample'), array_keys($toolkits));
  140. $output = theme('table', array('header' => $header, 'rows' => $test_table, 'id' => 'imagecache-testsuite'));
  141. // Default system zebra-striping fails to show my transparency on white
  142. drupal_add_html_head('<style type="text/css" >#imagecache-testsuite tr.even{background-color:#EEEEEE !important;} #imagecache-testsuite td{vertical-align:top;} #imagecache-testsuite tr.error{background-color:#FFCCCC !important;}</style>');
  143. return $output;
  144. }
  145. else {
  146. // Run the process and return the image.
  147. // @see image_style_create_derivative()
  148. $style = $tests[$test_id];
  149. if (! $style) {
  150. trigger_error("Unknown test style preset '$test_id' ", E_USER_ERROR);
  151. return FALSE;
  152. }
  153. // Start emulating image_style_create_derivative()
  154. // The main difference being I determine the toolkit I want to use.
  155. // SOME of this code is probably redundant, was a lot of copy&paste without true understanding of the new image.module
  156. $image_uri = $target;
  157. if (!$image = image_load($target, $toolkit)) {
  158. trigger_error("Failed to open original image $target with toolkit $toolkit", E_USER_ERROR);
  159. return FALSE;
  160. }
  161. // Need to save the result before returning it - to stay compatible with imagemagick
  162. $filename = "$test_id-$toolkit.{$image->info['extension']}";
  163. $derivative_uri = image_style_path($style['name'], $filename);
  164. file_prepare_directory(dirname($derivative_uri), FILE_CREATE_DIRECTORY);
  165. watchdog('imagecache_testsuite', 'Checking a save dir %dir', array('%dir' => dirname($derivative_uri)), WATCHDOG_DEBUG);
  166. // Imagemagick is not quite right? place a file where the file is supposed to go
  167. // before I put the real path there? else drupal_realpath() says nuh.
  168. #file_save_data('touch this for imagemagick', $derivative_uri, FILE_EXISTS_REPLACE);
  169. foreach ($style['effects'] as $effect) {
  170. // Need to load the full effect definitions, our test ones don't know all the callback info
  171. $effect_definition = image_effect_definition_load($effect['name']);
  172. if (empty($effect_definition)) {
  173. watchdog('imagecache_testsuite', 'I have no idea what %name is', array('%name' => $full_effect['name']), WATCHDOG_ERROR);
  174. continue;
  175. }
  176. $full_effect = array_merge($effect_definition, array('data' => $effect['data']));
  177. if (! image_effect_apply($image, $full_effect)) {
  178. watchdog('imagecache_testsuite', 'action: %action (%callback) failed for %src', array('%action' => $full_effect['label'], '%src' => $target, '%callback' => $full_effect['effect callback']), WATCHDOG_ERROR);
  179. #return FALSE;
  180. }
  181. }
  182. #watchdog('imagecache_testsuite', "processed $test_id-$toolkit, ready to save", array(), WATCHDOG_DEBUG);
  183. #watchdog('imagecache_testsuite', print_r($image, 1));
  184. #dpm(get_defined_vars());
  185. if (!image_save($image, $derivative_uri)) {
  186. watchdog('imagecache_testsuite', 'saving image %label failed for %derivative_uri', array('%derivative_uri' => $derivative_uri, '%label' => $effect['label']), WATCHDOG_ERROR);
  187. return FALSE;
  188. }
  189. if ($result_image = image_load($derivative_uri)) {
  190. #watchdog('imagecache_testsuite', 'transferring result', array(), WATCHDOG_DEBUG);
  191. file_transfer($result_image->source, array('Content-Type' => $result_image->info['mime_type'], 'Content-Length' => $result_image->info['file_size']));
  192. drupal_exit();
  193. }
  194. return "Failed to load the expected result from $derivative_uri";
  195. }
  196. }
  197. /**
  198. * Implements hook_image_default_styles().
  199. *
  200. * Loads the individual test cases and makes them available as enabled styles
  201. */
  202. function imagecache_testsuite_image_default_styles() {
  203. return imagecache_testsuite_get_tests();
  204. }
  205. /**
  206. * Retrieve the list of presets, each of which contain actions and action
  207. * definitions.
  208. *
  209. * Scans all the module folders for files named *.imagecache_preset.inc
  210. *
  211. * It seems that the required shape in D7 is
  212. * $style=>array(
  213. * 'effects' => array(
  214. * 0 => array('name' => 'something', 'data' => array())
  215. * )
  216. * )
  217. */
  218. function imagecache_testsuite_get_tests() {
  219. $presets = array();
  220. $folders = imagecache_testsuite_get_folders();
  221. foreach ($folders as $folder) {
  222. $preset_files = file_scan_directory($folder, "/.*.imagecache_preset.inc/");
  223. // Setting filepath in this scope allows the tests to know where they are.
  224. // The inc files may use it to create their rules.
  225. $filepath = $folder;
  226. foreach($preset_files as $preset_file) {
  227. include_once($preset_file->uri);
  228. }
  229. }
  230. uasort($presets, 'element_sort');
  231. return $presets;
  232. }
  233. /**
  234. * Places to scan for test presets and sample images.
  235. *
  236. * @return an array of foldernames of everything that implements
  237. * imagecache_actions.
  238. */
  239. function imagecache_testsuite_get_folders() {
  240. $folders = array(drupal_get_path('module', 'imagecache_testsuite'));
  241. foreach (module_implements('image_effect_info') as $module_name) {
  242. $folders[] = drupal_get_path('module', $module_name) . '/tests';
  243. }
  244. return $folders;
  245. }
  246. /**
  247. * Display a page demonstrating a number of positioning tests
  248. *
  249. * Tests both styles of positioning - the x=, y= original, used in most places,
  250. * pls the css-like left=, top= version also.
  251. */
  252. function imagecache_testsuite_positioning() {
  253. drupal_set_title("Testing the positioning algorithm");
  254. $tests = imagecache_testsuite_positioning_get_tests();
  255. $table = array();
  256. // $dst_image represents tha field or canvas.
  257. // $src_image is the item being placed on it.
  258. // Both these represent an imageapi-type image resource handle, but contain just dimensions
  259. $src_image->info = array('width' => 75, 'height' => 100);
  260. $dst_image->info = array('width' => 200, 'height' => 150);
  261. foreach ($tests as $testname => $test) {
  262. // calc it, using either old or new method
  263. if (isset($test['parameters']['x']) || isset($test['parameters']['y'])) {
  264. $result['x'] = imagecache_actions_keyword_filter($test['parameters']['x'], $dst_image->info['width'], $src_image->info['width']);
  265. $result['y'] = imagecache_actions_keyword_filter($test['parameters']['y'], $dst_image->info['height'], $src_image->info['height']);
  266. }
  267. else {
  268. // use css style
  269. $result = imagecache_actions_calculate_relative_position($dst_image, $src_image, $test['parameters']);
  270. }
  271. $expected_illustration = theme_positioning_test($test['expected']['x'], $test['expected']['y']);
  272. $result_illustration = theme_positioning_test($result['x'], $result['y']);
  273. $row = array();
  274. $row['name'] = array('data' => '<h3>' . $testname . '</h3>' . $test['description']);
  275. $row['parameters'] = theme_positioning_parameters($test['parameters']);
  276. $row['expected'] = theme_positioning_parameters($test['expected']);
  277. $row['expected_image'] = $expected_illustration;
  278. $row['result'] = theme_positioning_parameters($result);
  279. $row['result_image'] = $result_illustration;
  280. $table[] = $row;
  281. }
  282. return 'Result of test:'. theme('table', array('test', 'parameters', 'expected', 'image', 'result', 'actual image', 'status'), $table);
  283. }
  284. function theme_positioning_test($x, $y) {
  285. $inner = "<div style='background-color:red; width:75px; height:100px; position:absolute; left:{$x}px; top:{$y}px'>";
  286. $outer = "<div style='background-color:blue; width:200px; height:150px; position:absolute; left:25px; top:25px'><div style='position:relative'>$inner</div></div>";
  287. $wrapper = "<div style='background-color:#CCCCCC; width:250px; height:200px; position:relative'>$outer</div>";
  288. return $wrapper;
  289. }
  290. function theme_positioning_parameters($parameters) {
  291. foreach ($parameters as $key => $value) {
  292. $outputs[] = "[$key] => $value";
  293. }
  294. return '<pre>' . join("\n", $outputs) . '</pre>';
  295. }
  296. function imagecache_testsuite_positioning_get_tests() {
  297. return array(
  298. 'base' => array(
  299. 'parameters' => array(
  300. 'x' => '0',
  301. 'y' => '0',
  302. ),
  303. 'description' => '0 is top left.',
  304. 'expected' => array(
  305. 'x' => '0',
  306. 'y' => '0',
  307. ),
  308. ),
  309. 'numbers' => array(
  310. 'parameters' => array(
  311. 'x' => '50',
  312. 'y' => '-50',
  313. ),
  314. 'description' => 'Basic numbers indicate distance and direction from top left.',
  315. 'expected' => array(
  316. 'x' => '50',
  317. 'y' => '-50',
  318. ),
  319. ),
  320. 'keywords' => array(
  321. 'parameters' => array(
  322. 'x' => 'center',
  323. 'y' => 'bottom',
  324. ),
  325. 'description' => "Plain keywords will align against the region",
  326. 'expected' => array(
  327. 'x' => '62.5',
  328. 'y' => '50',
  329. ),
  330. ),
  331. 'keyword with offsets' => array(
  332. 'parameters' => array(
  333. 'x' => 'right+10',
  334. 'y' => 'bottom+10',
  335. ),
  336. 'description' => "Keywords can be used with offsets. Positive numbers are in from the named side",
  337. 'expected' => array(
  338. 'x' => '115',
  339. 'y' => '40',
  340. ),
  341. ),
  342. 'keyword with negative offsets' => array(
  343. 'parameters' => array(
  344. 'x' => 'right-10',
  345. 'y' => 'bottom-10',
  346. ),
  347. 'description' => "Negative numbers place the item outside the boundry",
  348. 'expected' => array(
  349. 'x' => '135',
  350. 'y' => '60',
  351. ),
  352. ),
  353. 'percent' => array(
  354. 'parameters' => array(
  355. 'x' => '50%',
  356. 'y' => '50%',
  357. ),
  358. 'description' => "Percentages on their own will CENTER on both the source and destination items",
  359. 'expected' => array(
  360. 'x' => '62.5',
  361. 'y' => '25',
  362. ),
  363. ),
  364. 'keyword with percent' => array(
  365. 'parameters' => array(
  366. 'x' => 'right+10%',
  367. 'y' => 'bottom+10%',
  368. ),
  369. 'description' => "Percentages can be used with keywords, though the placed image will be centered on the calculated position.",
  370. 'expected' => array(
  371. 'x' => '142.5',
  372. 'y' => '85',
  373. ),
  374. ),
  375. 'css styles' => array(
  376. 'parameters' => array(
  377. 'left' => '10px',
  378. 'bottom' => '10px',
  379. ),
  380. 'description' => "A different method uses css-like parameters.",
  381. 'expected' => array(
  382. 'x' => '10',
  383. 'y' => '40',
  384. ),
  385. ),
  386. 'css negatives' => array(
  387. 'parameters' => array(
  388. 'left' => '-10px',
  389. 'bottom' => '-10px',
  390. ),
  391. 'description' => "Negative numbers from sides always move outside of the boundries.",
  392. 'expected' => array(
  393. 'x' => '-10',
  394. 'y' => '60',
  395. ),
  396. ),
  397. 'css with percents' => array(
  398. 'parameters' => array(
  399. 'right' => '+10%',
  400. 'bottom' => '+10%',
  401. ),
  402. 'description' => "Using percents with sides calculates the percent location on the base, then centers the source item on that point.",
  403. 'expected' => array(
  404. 'x' => '142.5',
  405. 'y' => '85',
  406. ),
  407. ),
  408. 'css centering' => array(
  409. 'parameters' => array(
  410. 'right' => '50%',
  411. 'top' => '50%',
  412. ),
  413. 'description' => "The auto-centering that happens when percents are used means you can easily center things at 50%.",
  414. 'expected' => array(
  415. 'x' => '62.5',
  416. 'y' => '25',
  417. ),
  418. ),
  419. 'css positioning' => array(
  420. 'parameters' => array(
  421. 'right' => 'left+20',
  422. 'top' => 'bottom-20',
  423. ),
  424. 'description' => "It's also possible to use keywords there, though it's not smart to do so",
  425. 'expected' => array(
  426. 'x' => '-55',
  427. 'y' => '130',
  428. ),
  429. ),
  430. );
  431. }