You are here

textimage.module in Textimage 6.2

File

textimage.module
View source
<?php

/**
 * @file
 */

/**
 * Matches all 'P' Unicode character classes (punctuation)
 */
define('TEXTIMAGE_PREG_CLASS_PUNCTUATION', '\\x{21}-\\x{23}\\x{25}-\\x{2a}\\x{2c}-\\x{2f}\\x{3a}\\x{3b}\\x{3f}\\x{40}\\x{5b}-\\x{5d}' . '\\x{5f}\\x{7b}\\x{7d}\\x{a1}\\x{ab}\\x{b7}\\x{bb}\\x{bf}\\x{37e}\\x{387}\\x{55a}-\\x{55f}' . '\\x{589}\\x{58a}\\x{5be}\\x{5c0}\\x{5c3}\\x{5f3}\\x{5f4}\\x{60c}\\x{60d}\\x{61b}\\x{61f}' . '\\x{66a}-\\x{66d}\\x{6d4}\\x{700}-\\x{70d}\\x{964}\\x{965}\\x{970}\\x{df4}\\x{e4f}' . '\\x{e5a}\\x{e5b}\\x{f04}-\\x{f12}\\x{f3a}-\\x{f3d}\\x{f85}\\x{104a}-\\x{104f}\\x{10fb}' . '\\x{1361}-\\x{1368}\\x{166d}\\x{166e}\\x{169b}\\x{169c}\\x{16eb}-\\x{16ed}\\x{1735}' . '\\x{1736}\\x{17d4}-\\x{17d6}\\x{17d8}-\\x{17da}\\x{1800}-\\x{180a}\\x{1944}\\x{1945}' . '\\x{2010}-\\x{2027}\\x{2030}-\\x{2043}\\x{2045}-\\x{2051}\\x{2053}\\x{2054}\\x{2057}' . '\\x{207d}\\x{207e}\\x{208d}\\x{208e}\\x{2329}\\x{232a}\\x{23b4}-\\x{23b6}\\x{2768}-' . '\\x{2775}\\x{27e6}-\\x{27eb}\\x{2983}-\\x{2998}\\x{29d8}-\\x{29db}\\x{29fc}\\x{29fd}' . '\\x{3001}-\\x{3003}\\x{3008}-\\x{3011}\\x{3014}-\\x{301f}\\x{3030}\\x{303d}\\x{30a0}' . '\\x{30fb}\\x{fd3e}\\x{fd3f}\\x{fe30}-\\x{fe52}\\x{fe54}-\\x{fe61}\\x{fe63}\\x{fe68}' . '\\x{fe6a}\\x{fe6b}\\x{ff01}-\\x{ff03}\\x{ff05}-\\x{ff0a}\\x{ff0c}-\\x{ff0f}\\x{ff1a}' . '\\x{ff1b}\\x{ff1f}\\x{ff20}\\x{ff3b}-\\x{ff3d}\\x{ff3f}\\x{ff5b}\\x{ff5d}\\x{ff5f}-' . '\\x{ff65}');

/**
 * Matches all 'Z' Unicode character classes (separators)
 */
define('TEXTIMAGE_PREG_CLASS_SEPARATOR', '\\x{20}\\x{a0}\\x{1680}\\x{180e}\\x{2000}-\\x{200a}\\x{2028}\\x{2029}\\x{202f}\\x{205f}\\x{3000}');
define('TEXTIMAGE_ALIGN_LEFT', 1);
define('TEXTIMAGE_ALIGN_CENTER', 2);
define('TEXTIMAGE_ALIGN_RIGHT', 3);

/**
 * Implementation of hook_help().
 */
function textimage_help($path, $arg) {
  $output = "";
  switch ($path) {
    case 'admin/help#textimage':
      $output = '
        <h2 id="textimage-introduction">The dynamic text to image generator!</h2>
        <p>
          Textimage adds text to image functionality using GD2 and Freetype, enabling users to create crisp images on the fly for use as theme objects, headings or limitless other possibilities.
        </p>
        <h2>Installing Fonts</h2>
        <p>
          Before you can begin using Textimage you must upload at least one TrueType or OpenType font to the server and tell Textimage where you uploaded it. Fonts must have a .tff or .otf extension to be seen by Textimage. If you do not have any TrueType or OpenType fonts, you can download some free GNU fonts from the <a href="http://savannah.nongnu.org/projects/freefont/">Free UCS Outline Fonts Project</a>. Once the fonts are uploaded, enter the UNIX-style path the fonts on the <a href="!config">configuration page</a>.
        </p>
        <h2 id="textimage-configuration">Configuration</h2>
        <p>
          The basis of Textimage is made of configuration options called <em>presets</em>. A preset defines what font, size, color, etc. should be used in the generated image. You can create new presets on the <a href="!presets">presets page</a>. Most options are pretty self explanatory, but background images can get pretty complicated if you begin to use other presets as backgrounds. Let\'s run through an example.
        </p>
        <p>
          If you specified a backgrounds directory on the main configuration page, a list of backgrounds is automatically popuplated into the Background Image select list. Let\'s say there\'s a image called &quot;header.png&quot; in the image list that looks like this:
        </p>
        <p>
          <img src="!example1" alt="example1" />
        </p>
        <p>
          Now we\'ll create a preset called &quot;preset1&quot;. In this preset, set the font to Braggadocio (not included), 54px, #FFFFFF (white) color. Select header.png from the background list and position the text at x-offset 14 and y-offset 22 (in pixels). After the preset is saved, Textimage is now ready to automatically generate images based on text strings. You could directly visit the image at {files}/textimage/preset1/Hello.png (where {files} is your sites file directory) and get the following result:
        </p>
        <p>
          <img src="!example2" alt="example2" />
        </p>
        <p>
          To get crazy now, create a new preset called &quot;preset2&quot;. In this preset, set the font to Century (also not included), 20px, #000000 (black) color. Select <em>preset1</em> from the background list and position the text at x-offset 14 and y-offset 94. Save the preset then visit {files}/textimage/preset2/Hello/world!.png and get the following result:
        </p>
        <p>
          <img src="!example3" alt="example3" />
        </p>
        <p>
          The entire preset1 is generated using the first argument <em>Hello</em>. Then preset2 is generated using the name of the file <em>world!.png</em>. You could continue chaining presets together over and over again.
        </p>
        <h2>File Names and Storage</h2>
        <p>
          Textimage supports .png, .gif, and .jpg input and output files. You can change the output format of the image simply by changing the extension of the last file. In the above example we made PNG images. If we had appended it with .jpg, a JPG image would have been created. Only PNG and GIF files support transparent backgrounds.
        </p>
      ';
      $output = t($output, array(
        '!config' => url('admin/build/textimage/settings'),
        '!presets' => url('admin/build/textimage/presets'),
        '!example1' => base_path() . drupal_get_path('module', 'textimage') . '/misc/example1.png',
        '!example2' => base_path() . drupal_get_path('module', 'textimage') . '/misc/example2.png',
        '!example3' => base_path() . drupal_get_path('module', 'textimage') . '/misc/example3.png',
      ));
      break;
    case 'admin/build/modules#description':
    case 'admin/build/modules/textimage':
    case 'admin/build/textimage':
      $output = t('Provides text to image manipulations.');
      break;
  }
  return $output;
}

/**
 * Implementation of hook_theme().
 */
function textimage_theme() {
  $theme = array(
    'textimage_image' => array(
      'arguments' => array(
        'preset',
        'text',
        'additional_text' => array(),
        'format' => 'png',
        'alt' => '',
        'title' => '',
        'attributes' => array(),
        'getsize' => TRUE,
        'image' => TRUE,
      ),
    ),
    'textimage_preset_edit' => array(
      'arguments' => array(
        'form' => array(),
      ),
      'file' => 'textimage_admin.inc',
    ),
  );
  module_load_include('inc', 'textimage', 'textimage_admin');
  foreach (textimage_get_presets() as $preset) {
    $theme['textimage_formatter_textimage_' . $preset->name] = array(
      'arguments' => array(
        'element' => NULL,
      ),
      'function' => 'theme_textimage_formatter',
    );
  }
  return $theme;
}

/**
 * Implementation of hook_menu().
 */
function textimage_menu() {
  $items = array();
  $items[file_directory_path() . '/textimage'] = array(
    'page callback' => 'textimage_image',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/textimage'] = array(
    'title' => 'Textimage',
    'description' => 'Configure text to image preset functions.',
    'page callback' => 'textimage_preset_list',
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/build/textimage/preset'] = array(
    'title' => 'Presets',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/build/textimage/preset/list'] = array(
    'title' => 'List',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/build/textimage/preset/new'] = array(
    'title' => 'New',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'textimage_preset_edit',
      'new',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/build/textimage/preset/%/edit'] = array(
    'title' => 'Edit Preset',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'textimage_preset_edit',
      'edit',
      4,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/textimage/preset/%/delete'] = array(
    'title' => 'Edit Preset',
    'load arguments' => array(
      4,
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'textimage_preset_delete_confirm',
      4,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/textimage/preset/%/flush'] = array(
    'title' => 'Flush Preset Cache',
    'load arguments' => array(
      4,
    ),
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'textimage_preset_flush_confirm',
      4,
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items['admin/build/textimage/settings'] = array(
    'title' => 'Settings',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'textimage_settings_form',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'textimage_admin.inc',
    'weight' => 0,
    'type' => MENU_LOCAL_TASK,
  );
  $items['js/textimage'] = array(
    'page callback' => 'textimage_js',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implementation of hook_perm().
 */
function textimage_perm() {
  return array(
    'create textimages',
  );
}

/**
 * Menu Callback function converts the current textimage path into an image. On
 * first request, the image is returned to the browser from Drupal and then
 * saved to the disk. On subsequent requests (with Clean URLs enabled), the
 * cached image is loaded directly.
 *
 * This function takes a dynamic number of arguments.
 *
 * @param $preset_name
 *   The name of the preset to be used in this textimage
 * @param ...
 *   An unlimited number of additional text parameters to be used as the
 *   display text for textimages displayed on top of one another. Only used
 *   if the current preset has the its Background Image option set to the
 *   result of another preset. Text is used in reverse order. So the last
 *   directory will be the first chained preset used.
 * @param $text
 *   The text to be displayed in this preset with the output format
 *   appended as the file extension. For example, 'sample.png' will output a
 *   PNG with the text 'sample'. 'sample.jpg' will output the same image but
 *   in JPG format.
 *
 */
function textimage_image() {
  $pattern = '/' . str_replace('/', '\\/', base_path() . '(\\?q=)?' . file_directory_path() . '/textimage/') . '/';
  $args = explode('/', preg_replace($pattern, '', request_uri()));
  $preset = array_shift($args);
  if (is_numeric($preset)) {
    drupal_not_found();
    exit;
  }
  $filename = urldecode(array_pop($args));
  $additional_text = $args;

  // Determine our output format
  preg_match('/\\.([a-z]+)$/i', $filename, $matches);
  $format = $matches[1];
  if ($format == 'jpg') {
    $format = 'jpeg';
  }

  // Determine the text to display
  $text = preg_replace('/\\.([a-z]+)$/i', '', $filename);
  if (!($img = textimage_build_image('url', $preset, $text, $additional_text, $format))) {
    return FALSE;
  }
  drupal_goto($img);
}
function textimage_build_image($method, $preset, $text, $additional_text = array(), $format = 'png') {

  // Integrety check
  $output_function = 'image' . $format;
  if (!function_exists($output_function)) {
    $message = t('Unable to generate Textimage as the file extension is unsupported on this system. Files must have a .png, .jpg, or .gif extension.');
    watchdog('textimage', $message, array(), WATCHDOG_ERROR);
    drupal_set_message($message, 'error');
    return FALSE;
  }

  // Load preset
  if (!is_array($preset) && !($preset = _textimage_preset_load($preset))) {
    $message = t('Unable to generate Textimage as the preset %preset is not defined.', array(
      '%preset' => $preset,
    ));
    watchdog('textimage', $message, array(), WATCHDOG_ERROR);
    drupal_set_message($message, 'error');
    return FALSE;
  }
  $result = db_query("SELECT file FROM {textimage_image} WHERE pid = %d AND data = '%s'", $preset['pid'], serialize(array(
    'format' => $format,
    'text' => $text,
    'additional_text' => $additional_text,
  )));
  $result = db_fetch_object($result);
  if (!$result && (user_access('create textimages') || $method == 'theme') || $preset['pid'] == 0) {

    // Generate the image resource
    $img = textimage_image_from_preset($preset, $text, $additional_text);
    $filename = time() . rand(1000, 9999) . '.' . $format;
    $directory = file_directory_path() . '/textimage/' . $preset['pid'];

    // Save the result so we don't have to recreate
    textimage_directory_check($directory);
    $output_function($img, $directory . '/' . $filename);
    imagedestroy($img);
    if ($preset['pid'] != 0) {
      db_query("INSERT INTO {textimage_image} (pid, file, data) VALUES ('%s', '%s', '%s')", $preset['pid'], $directory . '/' . $filename, serialize(array(
        'format' => $format,
        'text' => $text,
        'additional_text' => $additional_text,
      )));
    }
    return $directory . '/' . $filename;
  }
  elseif (!$result) {
    drupal_access_denied();
    return FALSE;
  }
  else {
    return $result->file;
  }
}

/**
 * Loads the Textimage preset and generates the GD image resource.
 *
 * @param $pid
 *   The id of the preset to be used in this textimage
 * @param $text
 *   The text to be displayed in this preset
 * @param $additional_text
 *   An array of text to be used in subsequent textimages. Only used if this
 *   preset uses the result of another preset as its background image.
 */
function textimage_image_from_preset($preset, $text, $additional_text = array()) {
  foreach ($preset['settings'] as $key => $settings) {
    if (is_array($settings)) {
      foreach ($settings as $name => $value) {
        ${$key . '_' . $name} = $value;
      }
    }
  }
  $text_fixed_width = isset($text_fixed_width) ? $text_fixed_width : 0;

  // Set font path
  $font_file = variable_get('textimage_fonts_path', drupal_get_path('module', 'textimage') . '/fonts') . '/' . $font_file;

  // Convert text case
  switch ($text_case) {
    case 'upper':
      $text = drupal_strtoupper($text);
      break;
    case 'lower':
      $text = drupal_strtolower($text);
      break;
    case 'ucfirst':
      $text = drupal_ucfirst($text);
      break;
    case 'ucwords':
      $text = preg_replace('/\\s(\\w+)\\b/e', "drupal_ucfirst('\$1')", $text);
      break;
  }

  // Generate the textimage
  $img = textimage_text_to_image($text, $font_size, $font_file, $font_color, $text_angle, $text_maximum_width, $text_fixed_width, $text_align);

  // Add a border
  if ($text_stroke['width'] && $text_stroke['color']) {
    $img = textimage_image_add_stroke($img, $text_stroke['width'], $text_stroke['color'], $font_color['opacity']);
  }

  // Add margin
  if ($text_margin['top'] || $text_margin['right'] || $text_margin['bottom'] || $text_margin['left']) {
    $img = textimage_image_add_margin($img, $text_margin);
  }

  // Place result on top of another preset's result
  if (is_numeric($background_image) && ($background_image = _textimage_preset_load($background_image))) {
    $next_preset = $background_image;
    $next_text = array_pop($additional_text);
    if (empty($next_text)) {
      $next_text = $text;
    }
    $background_resource = textimage_image_from_preset($next_preset, $next_text, $additional_text);
    $text_width = imagesx($img);
    $text_height = imagesy($img);
    imagealphablending($background_resource, TRUE);
    imagecopy($background_resource, $img, $background_xoffset, $background_yoffset, 0, 0, $text_width, $text_height);
    imagesavealpha($background_resource, TRUE);
    $img = $background_resource;
  }
  elseif (is_file($background_image)) {
    $info = image_get_info($background_image);
    $background_resource = image_toolkit_invoke('open', array(
      $background_image,
      $info['extension'],
    ));
    $text_width = imagesx($img);
    $text_height = imagesy($img);
    imagealphablending($background_resource, TRUE);
    imagecopy($background_resource, $img, $background_xoffset, $background_yoffset, 0, 0, $text_width, $text_height);
    imagesavealpha($background_resource, TRUE);
    $img = $background_resource;
  }
  else {
    $background_resource = imagecreatetruecolor(imagesx($img), imagesy($img));
    $alpha = 0;
    if (!$background_color) {
      $alpha = 127;
      $background_color = '#FFFFFF';
    }
    list($r, $g, $b) = _textimage_hex2rgb($background_color);
    $back = imagecolorallocatealpha($background_resource, $r, $g, $b, $alpha);
    imagefill($background_resource, 0, 0, $back);
    imagealphablending($background_resource, TRUE);
    imagecopy($background_resource, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
    imagesavealpha($background_resource, TRUE);
    $img = $background_resource;
  }
  return $img;
}

/**
 * This function adds a margin (or border) around an existing image resource
 */
function textimage_image_add_margin($img, $margin) {
  $width = imagesx($img);
  $height = imagesy($img);

  // Create a new image for the background
  $back_img = imagecreatetruecolor($width + $margin['right'] + $margin['left'], $height + $margin['top'] + $margin['bottom']);
  $back = imagecolorallocatealpha($back_img, 0, 0, 0, 127);
  imagefill($back_img, 0, 0, $back);

  // Apply the source image ontop the background with the new margin
  imagecopy($back_img, $img, $margin['left'], $margin['top'], 0, 0, $width, $height);
  imagealphablending($back_img, TRUE);
  imagesavealpha($back_img, TRUE);
  return $back_img;
}

/**
 * Stroke function adds a solid color stroke around an image with a transparent
 * background.
 *
 * @param $img
 *   The gd image resource of the image to modify
 * @param $thickness
 *   The width of the stroke to apply
 * @param $color
 *   The color of the stroke to apply
 *
 * @todo Add $position parameter to allow the stroke to be applied 'inside',
 * 'middle', or 'outside'. outside is the only current behavior.
 */
function textimage_image_add_stroke($img, $thickness, $color, $text_opacity) {
  if ($thickness > 0) {
    $width = imagesx($img);
    $height = imagesy($img);

    // Create a new image which we'll lay over the original
    $border_img = imagecreatetruecolor($width, $height);
    $back = imagecolorallocatealpha($border_img, 0, 0, 0, 127);
    imagefill($border_img, 0, 0, $back);
    for ($x = 0; $x < $width; $x++) {
      for ($y = 0; $y < $height; $y++) {
        $c = imagecolorsforindex($img, imagecolorat($img, $x, $y));

        // Outside only modify pixels which are less opaque than the text opacity.
        if ($c['alpha'] > -($text_opacity - 100) / 100 * 127) {
          textimage_image_stroke_change_pixels($img, $border_img, $thickness, $color, $x, $y, $width, $height);
        }
      }
    }

    // Merge the images
    imagealphablending($img, TRUE);
    imagecopy($img, $border_img, 0, 0, 0, 0, $width, $height);
  }
  return $img;
}

/**
 * Utility function for image_stroke. Analyzes surrounding pixels and determines
 * opacity of a pixel at that x-y coordinate
 */
function textimage_image_stroke_change_pixels(&$img, &$border_img, $thickness, $color, $x, $y, $width, $height) {
  list($r, $g, $b) = _textimage_hex2rgb($color);
  $pixel = imagecolorsforindex($img, imagecolorat($img, $x, $y));

  // Preform a radial analysis of all pixels within the radius of $thickness pixels
  $degree_increment = 90 / $thickness;
  $radial_coords = array();
  for ($degrees = 0; $degrees <= 90; $degrees += $degree_increment) {
    $x_offset = round(cos($degrees) * $thickness);
    $y_offset = round(sin($degrees) * $thickness);

    // Add the coordinates for the corresponding pixel in each 90 degree quadrant
    $radial_coords[] = array(
      'x' => $x + $x_offset,
      'y' => $y + $y_offset,
    );
    $radial_coords[] = array(
      'x' => $x - $x_offset,
      'y' => $y + $y_offset,
    );
    $radial_coords[] = array(
      'x' => $x + $x_offset,
      'y' => $y - $y_offset,
    );
    $radial_coords[] = array(
      'x' => $x - $x_offset,
      'y' => $y - $y_offset,
    );
  }

  // Generate a total alpha level for all analyzed pixels
  $total_alpha = 0;
  $total_colors = 0;
  foreach ($radial_coords as $coords) {
    if ($coords['x'] >= 0 && $coords['y'] >= 0 && $coords['x'] < $width && $coords['y'] < $height) {
      $xy_color = imagecolorsforindex($img, imagecolorat($img, $coords['x'], $coords['y']));
    }
    else {

      // This analized pixel is outside the dimensions of the image, record as transparent
      $xy_color = array(
        'alpha' => '127',
      );
    }
    $total_alpha += $xy_color['alpha'];
    $total_colors++;
  }

  // Check that we're not in the middle of the image or in a blonk area
  if ($total_alpha < 127 * $total_colors && $total_alpha > 0) {

    // If we're on a semi-transparent pixel, blend the remaining amount with our border color
    if ($pixel['alpha'] < 127) {
      $alpha = 127 - $pixel['alpha'];
    }
    else {
      $alpha = 127 - (127 * $total_colors - $total_alpha);
    }
    $alpha = $alpha < 0 ? 0 : $alpha;
    $alpha = $alpha > 127 ? 127 : $alpha;

    // Apply the color to the border overlay image
    $color = imagecolorallocatealpha($border_img, $r, $g, $b, $alpha);
    imagesetpixel($border_img, $x, $y, $color);
  }
}

/**
 * Create the image directory relative to the 'files' dir - if user specified one
 * Won't allow form submit unless the directory exists & is writable
 *
 * @param $directory_path
 *   String containing the path of the directory to check.
 */
function textimage_directory_check($directory_path) {

  // create each directory necessary if it doesn't exist
  $directory_path = drupal_substr($directory_path, drupal_strlen(file_directory_path()) + 1);
  foreach (explode('/', $directory_path) as $dir) {
    $dirs[] = $dir;
    $dir = file_directory_path() . '/' . implode($dirs, '/');
    if (!file_check_directory($dir, FILE_CREATE_DIRECTORY)) {
      return FALSE;
    }
  }
  return TRUE;
}

/**
 * Helper function for wrapping text (measures width).
 */
function textimage_measure_text_width($text, $fontsize, $font) {
  $box = imagettfbbox($fontsize, 0, $font, $text);
  return abs($box[4] - $box[0]) + 4;
}

/**
 * Wrap text for rendering at a given width.
 */
function textimage_wrap_text($text, $fontsize, $font, $maximum_width) {

  // State variables for the search interval
  $end = 0;
  $begin = 0;
  $fit = $begin;

  // Note: we count in bytes for speed reasons, but maintain character boundaries.
  while (true) {

    // Find the next wrap point (always after trailing whitespace).
    if (drupal_preg_match('/[' . TEXTIMAGE_PREG_CLASS_PUNCTUATION . '][' . TEXTIMAGE_PREG_CLASS_SEPARATOR . ']*|[' . TEXTIMAGE_PREG_CLASS_SEPARATOR . ']+/u', $text, $match, PREG_OFFSET_CAPTURE, $end)) {
      $end = $match[0][1] + drupal_strlen($match[0][0]);
    }
    else {
      $end = drupal_strlen($text);
    }

    // Fetch text, removing trailing white-space and measure it.
    $line = preg_replace('/[' . TEXTIMAGE_PREG_CLASS_SEPARATOR . ']+$/u', '', drupal_substr($text, $begin, $end - $begin));
    $width = textimage_measure_text_width($line, $fontsize, $font);

    // See if $line extends past the available space.
    if ($width > $maximum_width) {

      // If this is the first word, we need to truncate it.
      if ($fit == $begin) {

        // Cut off letters until it fits.
        while (drupal_strlen($line) > 0 && $width > $maximum_width) {
          $line = drupal_substr($line, 0, -1);
          $width = textimage_measure_text_width($line, $fontsize, $font);
        }

        // If no fit was found, the image is too narrow..
        $fit = drupal_strlen($line) ? $begin + drupal_strlen($line) : $end;
      }

      // We have a valid fit for the next line. Insert a line-break and reset
      // the search interval.
      $text = drupal_substr($text, 0, $fit) . "\n" . drupal_substr($text, $fit);
      $end = $begin = ++$fit;
    }
    else {

      // We can fit this text. Wait for now.
      $fit = $end;
    }
    if ($end == drupal_strlen($text)) {

      // All text fits. No more changes are needed.
      break;
    }
  }
  return $text;
}

/**
 * Unicode-safe preg_match().
 *
 * Search subject for a match to the regular expression given in pattern,
 * but return offsets in characters, where preg_match would return offsets
 * in bytes.
 *
 * @see http://php.net/manual/en/function.preg-match.php
 */
if (!function_exists('drupal_preg_match')) {
  function drupal_preg_match($pattern, $subject, &$matches, $flags = NULL, $offset = 0) {

    // Convert the offset value from characters to bytes.
    $offset = strlen(drupal_substr($subject, 0, $offset, $encoding));
    $return_value = preg_match($pattern, $subject, $matches, $flags, $offset);
    if ($return_value && $flags & PREG_OFFSET_CAPTURE) {
      foreach ($matches as &$match) {

        // Convert the offset returned by preg_match from bytes back to characters.
        $match[1] = drupal_strlen(substr($subject, 0, $match[1]));
      }
    }
    return $return_value;
  }
}

/**
 * Generate an image containing text with the given parameters.
 *
 * @return $image
 *   A GD image resource.
 */
function textimage_text_to_image($text, $fontsize, $font, $color = array(
  'hex' => '#000000',
  'opacity' => '100',
), $angle = 0, $maximum_width = 0, $fixed_width = 0, $align = TEXTIMAGE_ALIGN_LEFT) {

  // Set rotation angle.
  $q_angle = -$angle;
  while ($q_angle > 0 || $q_angle <= -90) {
    $q_angle -= $q_angle > 0 ? 90 : -90;
  }
  while ($angle < 0 || $angle >= 360) {
    $angle += $angle < 0 ? 360 : -360;
  }
  $rotation = -(floor($angle / 90) * 90);

  // Calculate rotation variables.
  $rad = deg2rad($q_angle);
  $sin = -sin($rad);
  $cos = cos($rad);

  // Perform text wrapping, if necessary.
  if ($maximum_width - 1 > 0) {
    $text = textimage_wrap_text($text, $fontsize, $font, $maximum_width - 1);
  }

  // Get fixed and rotated boundry box co-ordinates.
  $bbox = _textimage_imagettfbbox($fontsize, $q_angle, $font, $text);

  // Calculate dimensions of text box.
  $text_width = sqrt(pow(abs($bbox[0] - $bbox[2]), 2) + pow(abs($bbox[1] - $bbox[3]), 2));
  $text_height = sqrt(pow(abs($bbox[0] - $bbox[6]), 2) + pow(abs($bbox[1] - $bbox[7]), 2));

  // Not used.
  // Calculate dimensions of box from text box.
  $box_width = max($bbox[0], $bbox[2], $bbox[4], $bbox[6]) - min($bbox[0], $bbox[2], $bbox[4], $bbox[6]);
  $box_height = max($bbox[1], $bbox[3], $bbox[5], $bbox[7]) - min($bbox[1], $bbox[3], $bbox[5], $bbox[7]);

  // Calculate dimensions of image.
  $image_width = $fixed_width && $maximum_width > 0 ? ($maximum_width - 1) * $cos + $text_height * $sin : $box_width;
  $image_height = $fixed_width && $maximum_width > 0 ? ($maximum_width - 1) * $sin + $text_height * $cos : $box_height;

  // Create Image.
  $image = imagecreatetruecolor($image_width + 1, $image_height + 1);
  $back = imagecolorallocatealpha($image, 0, 0, 0, 127);
  imagefill($image, 0, 0, $back);

  // Set text alignment left
  $x = -$bbox[0];
  $y = $box_height - $bbox[3];
  if ($fixed_width && $maximum_width > 0 && $align != TEXTIMAGE_ALIGN_LEFT) {
    switch ($align) {

      // Set text alignment center
      case TEXTIMAGE_ALIGN_CENTER:
        $x += ($image_width - $box_width) / 2;
        $y += ($image_height - $box_height) / 2;
        break;

      // Set text alignment right
      case TEXTIMAGE_ALIGN_RIGHT:
        $x += $image_width - $box_width;
        $y += $image_height - $box_height;
        break;
    }
  }

  // Create the textimage.
  list($r, $g, $b) = _textimage_hex2rgb($color['hex']);
  $alpha = -($color['opacity'] - 100) / 100 * 127;
  $fore = imagecolorallocatealpha($image, $r, $g, $b, $alpha);
  imagettftext($image, $fontsize, $q_angle, $x, $y, $fore, $font, $text);
  if ($rotation != 0) {
    $image = imagerotate($image, $rotation, 0);
  }

  // Preserve transparency settings.
  imagealphablending($image, TRUE);
  imagesavealpha($image, TRUE);
  return $image;
}
function _textimage_imagettfbbox($fontsize, $angle, $font, $text) {

  // Get standard boundary box co-ordinates.
  $coords = imagettfbbox($fontsize, 0, $font, $text);

  // Get boundary box co-ordinates for firstline of text.
  $firstline_coords = $coords;
  if (strstr($text, "\n")) {
    $lines = explode("\n", $text);
    $firstline_coords = imagettfbbox($fontsize, 0, $font, $lines[0]);
  }

  // Get boundary box co-ordinates for baseline characters.
  $baseline_coords = imagettfbbox($fontsize, 0, $font, join(drupal_map_assoc(range(33, 122), 'chr')));

  // Adjust boundary box co-ordinates.
  $yoffset = $firstline_coords[1] - $firstline_coords[5] >= $fontsize ? $baseline_coords[1] + 1 - $fontsize * 0.3 : 0;
  $coords[0] = 0;
  $coords[1] -= $yoffset;
  $coords[2] = $coords[2] - $baseline_coords[6];
  $coords[3] -= $yoffset;
  $coords[4] = $coords[4] - $baseline_coords[0];
  $coords[5] -= $yoffset;
  $coords[6] = 0;
  $coords[7] -= $yoffset;

  // Calculate rotation variables.
  $rad = deg2rad($angle);
  $sin = -sin($rad);
  $cos = cos($rad);

  // Rotate boundary box co-ordinates.
  $bbox = array();
  for ($i = 0; $i < 7; $i += 2) {
    $bbox[$i] = round($coords[$i] * $cos + $coords[$i + 1] * -$sin);
    $bbox[$i + 1] = round($coords[$i + 1] * $cos - $coords[$i] * -$sin);
  }

  // Return boundary box;
  return $bbox;
}
if (!function_exists('imagerotate')) {
  function imagerotate($im, $angle, $bgcolor) {
    if ($angle === 0) {
      return $im;
    }

    // imagerotate() in php's libgd rotates the image counterclockwise,
    // this implementation rotates clockwise. The angle needs to be
    // inverted to give the same behaviour between these implementations.
    $angle = 360 + $angle;
    $width = imagesx($im);
    $height = imagesy($im);

    // background color.
    list($r, $g, $b, $a) = _textimage_hex2rgb($bgcolor);
    switch ($angle) {
      case 270:
      case 90:

        // flip dimensions.
        $rot_width = $height;
        $rot_height = $width;
        break;
      case 180:

        // maintain dims.
        $rot_width = $width;
        $rot_height = $height;
        break;
    }
    $rotate = imagecreatetruecolor($rot_width, $rot_height);
    $bg = imagecolorallocatealpha($rotate, $r, $g, $b, $a);
    imagefilledrectangle($rotate, 0, 0, $rot_width, $rot_height, $bg);
    imagealphablending($rotate, FALSE);
    imagesavealpha($rotate, TRUE);
    switch ($angle) {
      case 270:
        $rot_width--;
        for ($y = 0; $y < $height; ++$y) {
          for ($x = 0; $x < $width; ++$x) {
            imagesetpixel($rotate, $rot_width - $y, $x, imagecolorat($im, $x, $y));
          }
        }
        break;
      case 90:
        $rot_height--;
        for ($y = 0; $y < $height; ++$y) {
          for ($x = 0; $x < $width; ++$x) {
            imagesetpixel($rotate, $y, $rot_height - $x, imagecolorat($im, $x, $y));
          }
        }
        break;
      case 180:
        $rot_width--;
        $rot_height--;
        for ($y = 0; $y < $height; ++$y) {
          for ($x = 0; $x < $width; ++$x) {
            imagesetpixel($rotate, $rot_width - $x, $rot_height - $y, imagecolorat($im, $x, $y));
          }
        }
        break;
    }
    return $rotate;
  }
}

/**
 * load a preset by id or name.
 * @param preset
 *    preset id or name.
 */
function _textimage_preset_load($preset) {

  // Load preset by id
  if (is_numeric($preset)) {
    $preset = db_fetch_array(db_query('SELECT * FROM {textimage_preset} WHERE pid = %d', $preset));
  }
  else {
    $preset = db_fetch_array(db_query("SELECT * FROM {textimage_preset} WHERE name = '%s'", $preset));
  }
  if (empty($preset)) {
    return FALSE;
  }
  else {
    $preset['settings'] = unserialize($preset['settings']);
    return $preset;
  }
}

/**
 *  Convert a hex color representation to it's rgb integer components.
 *
 *  @param $hex
 *    Hex representation of the color.
 *    Can be in the formats: '#ABC','ABC','#AABBCC','AABBCC'
 *  @return
 *    Array with three components RGB.
 */
function _textimage_hex2rgb($hex) {
  $r = $g = $b = '';
  $hex = ltrim($hex, '#');
  if (preg_match('/^[0-9a-f]{3}$/i', $hex)) {

    // 'FA3' is the same as 'FFAA33' so r=FF, g=AA, b=33
    $r = str_repeat($hex[0], 2);
    $g = str_repeat($hex[1], 2);
    $b = str_repeat($hex[2], 2);
  }
  elseif (preg_match('/^[0-9a-f]{6}$/i', $hex)) {

    // #FFAA33 or r=FF, g=AA, b=33
    $r = drupal_substr($hex, 0, 2);
    $g = drupal_substr($hex, 2, 2);
    $b = drupal_substr($hex, 4, 2);
  }
  $r = hexdec($r);
  $g = hexdec($g);
  $b = hexdec($b);
  return array(
    $r,
    $g,
    $b,
  );
}

/**
 * Implementation of hook_field_formatter_info().
 */
function textimage_field_formatter_info() {
  $formatters = array();
  module_load_include('inc', 'textimage', 'textimage_admin');
  foreach (textimage_get_presets() as $preset) {
    $formatters['textimage_' . $preset->name] = array(
      'label' => t('Textimage:') . ' ' . $preset->name,
      'field types' => array(
        'text',
        'email',
      ),
    );
  }
  return $formatters;
}
function theme_textimage_formatter($element) {
  $alt = $title = $element['#item']['safe'];
  if (isset($element['#item']['email'])) {
    $alt = $title = '';
  }
  return theme('textimage_image', drupal_substr($element['#formatter'], 10), $element['#item']['safe'], array(), 'png', $alt, $title);
}

/**
 * Theme function for displaying textimages
 */
function theme_textimage_image($preset, $text, $additional_text = array(), $format = 'png', $alt = '', $title = '', $attributes = array(), $getsize = TRUE, $image = TRUE) {
  if (!($path = textimage_build_image('theme', $preset, $text, $additional_text, $format))) {
    return FALSE;
  }
  if ($image) {
    return theme('image', $path, $alt, $title, $attributes, $getsize);
  }
  return $path;
}

/**
 * Textimage AHAH functionality
 */
function textimage_js() {
  $output = '';
  switch (arg(2)) {
    case 'preview':
      textimage_js_preview($output);
      break;
    case 'background':
      textimage_js_background($output);
      break;
  }
  print drupal_json($output);
  exit;
}
function textimage_js_preview(&$output) {
  $text = $_POST['settings']['preview']['text']['default'];
  $additional_text = array();
  if (isset($_POST['settings']['preview']['text']['additional'])) {
    $additional_text = $_POST['settings']['preview']['text']['additional'];
    rsort($additional_text);
  }
  $preset = array(
    'pid' => 0,
    'name' => $_POST['name'],
    'settings' => $_POST['settings'],
  );
  $output = theme_textimage_image($preset, $text, $additional_text, 'png', $text, $text);
}
function textimage_js_background(&$output) {
  if (empty($_POST['form_build_id'])) {

    // Invalid request.
    drupal_set_message(t('An unrecoverable error occurred.'), 'error');
    print drupal_to_js(array(
      'data' => theme('status_messages'),
    ));
    exit;
  }

  // Build the new form.
  $form_state = array(
    'submitted' => FALSE,
  );
  $form_build_id = $_POST['form_build_id'];
  $form = form_get_cache($form_build_id, $form_state);
  if (!$form) {

    // Invalid form_build_id.
    drupal_set_message(t('An unrecoverable error occurred. This form was missing from the server cache. Try reloading the page and submitting again.'), 'error');
    print drupal_to_js(array(
      'data' => theme('status_messages'),
    ));
    exit;
  }

  // form_get_cache() doesn't yield the original $form_state,
  // but form_builder() does. Needed for retrieving the file array.
  $built_form = $form;
  $built_form_state = $form_state;
  $built_form += array(
    '#post' => $_POST,
  );
  $built_form = form_builder($_POST['form_id'], $built_form, $built_form_state);

  // Clean ids, so that the same element doesn't get a different element id
  // when rendered once more further down.
  form_clean_id(NULL, TRUE);
  foreach (element_children($form['settings']['preview']['text']) as $key) {
    if ($key != 'default') {
      unset($form['settings']['preview']['text'][$key]);
    }
  }
  if (is_numeric($_POST['settings']['background']['image']) && ($preset = _textimage_preset_load($_POST['settings']['background']['image']))) {
    if (!isset($form['settings']['preview']['text']['additional'])) {
      $form['settings']['preview']['text']['additional'] = array();
    }
    _textimage_js_background($form);
    while (is_numeric($preset['settings']['background']['image']) && ($preset = _textimage_preset_load($preset['settings']['background']['image']))) {
      _textimage_js_background($form);
    }
  }
  form_set_cache($form_build_id, $form, $form_state);
  $form += array(
    '#post' => $_POST,
    '#programmed' => FALSE,
  );
  $form = form_builder($_POST['form_id'], $form, $form_state);
  $output = drupal_render($form['settings']['preview']['text']);
}
function _textimage_js_background(&$form) {
  $id = count($form['settings']['preview']['text']['additional']);
  $form['settings']['preview']['text']['additional'][$id] = $form['settings']['preview']['text']['default'];
  $form['settings']['preview']['text']['additional'][$id]['#title'] = 'Additional text';
}

Functions

Namesort descending Description
textimage_build_image
textimage_directory_check Create the image directory relative to the 'files' dir - if user specified one Won't allow form submit unless the directory exists & is writable
textimage_field_formatter_info Implementation of hook_field_formatter_info().
textimage_help Implementation of hook_help().
textimage_image Menu Callback function converts the current textimage path into an image. On first request, the image is returned to the browser from Drupal and then saved to the disk. On subsequent requests (with Clean URLs enabled), the cached image is loaded directly.
textimage_image_add_margin This function adds a margin (or border) around an existing image resource
textimage_image_add_stroke Stroke function adds a solid color stroke around an image with a transparent background.
textimage_image_from_preset Loads the Textimage preset and generates the GD image resource.
textimage_image_stroke_change_pixels Utility function for image_stroke. Analyzes surrounding pixels and determines opacity of a pixel at that x-y coordinate
textimage_js Textimage AHAH functionality
textimage_js_background
textimage_js_preview
textimage_measure_text_width Helper function for wrapping text (measures width).
textimage_menu Implementation of hook_menu().
textimage_perm Implementation of hook_perm().
textimage_text_to_image Generate an image containing text with the given parameters.
textimage_theme Implementation of hook_theme().
textimage_wrap_text Wrap text for rendering at a given width.
theme_textimage_formatter
theme_textimage_image Theme function for displaying textimages
_textimage_hex2rgb Convert a hex color representation to it's rgb integer components.
_textimage_imagettfbbox
_textimage_js_background
_textimage_preset_load load a preset by id or name.

Constants

Namesort descending Description
TEXTIMAGE_ALIGN_CENTER
TEXTIMAGE_ALIGN_LEFT
TEXTIMAGE_ALIGN_RIGHT
TEXTIMAGE_PREG_CLASS_PUNCTUATION Matches all 'P' Unicode character classes (punctuation)
TEXTIMAGE_PREG_CLASS_SEPARATOR Matches all 'Z' Unicode character classes (separators)