geeky · php code

Dynamically generate svg and converting it to gif in drupal

For the recent baby names project, we have a need to generate a baby name graphic on the fly to be used for pinterest. The corresponding graphics designed are very vector friendly. My first thought was why not generate these in svg to be shared?

There are three background patterns and three different color groups to produce a total of 9 variations. I played with the graphics provided by the designer and was able to produce a svg file template.

Using defs the pattern can be used as the svg fill background e.g. fill=”url(#pattern)”. Fill colors will be different for each gender ‘Boy’, ‘Girl’ or ‘Either’. Baby name text and definition can be passed in dynamically.

Using drupal 7 template engine, the various pieces of the svg file will be split up and then glued back.

First, set up the themes:

'baby_names_svg_parents_logo' => array(
  'variables' => array(
    'color' => NULL,
  ),
  'template' => 'templates/svg.parents-logo',
),
'baby_names_svg_pattern_geo' => array(
  'variables' => array(
    'pattern_color1' => NULL,
    'pattern_color2' => NULL,
    'background_color' => NULL,
  ),
  'template' => 'templates/svg.pattern-geo',
),
'baby_names_svg_pattern_stripe' => array(
  'variables' => array(
    'pattern_color' => NULL,
    'background_color' => NULL,
  ),
  'template' => 'templates/svg.pattern-stripe',
),
'baby_names_svg_pattern_dotted' => array(
  'variables' => array(
    'pattern_color' => NULL,
    'background_color' => NULL,
  ),
  'template' => 'templates/svg.pattern-dotted',
),
'baby_names_name_card' => array(
  'variables' => array(
    'name' => NULL,
    'name_color' => NULL,
    'meaning' => NULL,
    'gender' => NULL,
    'origins' => array(),
    'popularity' => NULL,
    'pattern' => array(),
    'name_info_lines' => array(),
    'parents_logo' => array(),
  ),
  'file' => 'includes/baby_names.name-card.inc',
  'template' => 'templates/svg.name-card',
),

Each template will contain svg markup

svg.pattern-dotted.tpl.php

<pattern id="pattern" x="10" y="10" width="60" height="60" patternUnits="userSpaceOnUse">
  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
    <g transform="translate(-237.000000, -302.000000)">
      <g transform="translate(237.000000, 302.000000)">
        <polygon class="background"
                 fill="<?php print $background_color; ?>"
                 points="0 60 60 60 60 0 0 0"/>
        <path class="pattern"
              fill="<?php print $pattern_color; ?>"
              d="M12 7.7C12 10.2 10 12.2 7.5 12.2 5 12.2 3 10.2 3 7.7 3 5.3 5 3.2 7.5 3.2 10 3.2 12 5.3 12 7.7M27 22.7C27 25.2 25 27.2 22.5 27.2 20 27.2 18 25.2 18 22.7 18 20.3 20 18.2 22.5 18.2 25 18.2 27 20.3 27 22.7M42 7.7C42 10.2 40 12.2 37.5 12.2 35 12.2 33 10.2 33 7.7 33 5.3 35 3.2 37.5 3.2 40 3.2 42 5.3 42 7.7M57 22.7C57 25.2 55 27.2 52.5 27.2 50 27.2 48 25.2 48 22.7 48 20.3 50 18.2 52.5 18.2 55 18.2 57 20.3 57 22.7M12 37.7C12 40.2 10 42.2 7.5 42.2 5 42.2 3 40.2 3 37.7 3 35.3 5 33.2 7.5 33.2 10 33.2 12 35.3 12 37.7M27 52.7C27 55.2 25 57.2 22.5 57.2 20 57.2 18 55.2 18 52.7 18 50.3 20 48.2 22.5 48.2 25 48.2 27 50.3 27 52.7M42 37.7C42 40.2 40 42.2 37.5 42.2 35 42.2 33 40.2 33 37.7 33 35.3 35 33.2 37.5 33.2 40 33.2 42 35.3 42 37.7M57 52.7C57 55.2 55 57.2 52.5 57.2 50 57.2 48 55.2 48 52.7 48 50.3 50 48.2 52.5 48.2 55 48.2 57 50.3 57 52.7"/>
      </g>
    </g>
  </g>
</pattern>

svg.pattern-geo.tpl.php

<pattern id="pattern" x="10" y="10" width="60" height="60" patternUnits="userSpaceOnUse">
  <g transform="translate(-334.000000, -149.000000)">
    <g transform="translate(334.000000, 148.000000)">
      <path class="background"
            fill="<?php print $background_color; ?>"
            d="M0 62.1c8.3 0 16.7 0 25 0 11 0 21.9 0 32.9 0 0.3 0 1.8 0.2 2.1 0 0.2-0.2 0-1.7 0-2 0-4.4 0-8.9 0-13.3 0-12.4 0-24.8 0-37.2 0-2.9 0-5.8 0-8.8 0-0.2-5.8-0.1-6.2-0.1 -12.1 0-24.3 0-36.4 0 -4.8 0-9.6 0-14.4 0 -0.9 0-1.9 0-2.8 0C-0.1 0.8 0 1.9 0 2.1c0 10.5 0 20.9 0 31.4 0 9.4 0 18.9 0 28.3C0 61.9 0 62 0 62.1z"/>
      <g transform="translate(0.000000, 1.000000)">
        <path class="left"
              fill="<?php print $pattern_color1; ?>"
              d="M15 0C10.6 8.9 6.1 17.8 1.7 26.7 1.1 27.8 0.6 28.9 0 30c9.7 0 19.3 0 29 0 0.3 0 0.7 0 1 0 -4.4-8.9-8.9-17.8-13.3-26.7C16.1 2.2 15.6 1.1 15 0z"/>
        <path class="right"
              fill="<?php print $pattern_color2; ?>"
              d="M45 60c4.4-8.9 8.9-17.8 13.3-26.7C58.9 32.2 59.4 31.1 60 30c-9.7 0-19.3 0-29 0 -0.3 0-0.7 0-1 0 4.4 8.9 8.9 17.8 13.3 26.7C43.9 57.8 44.4 58.9 45 60z"/>
      </g>
    </g>
  </g>
</pattern>

svg.pattern-stripe.tpl.php

<pattern id="pattern" x="10" y="10" width="16" height="16" patternUnits="userSpaceOnUse">
  <g transform="translate(-219.000000, -171.000000)">
    <g transform="translate(219.000000, 171.000000)">
      <path class="background"
            fill="<?php print $background_color; ?>"
            d="M0 16c4.9 0 9.9 0 14.8 0C15 16 16 16.1 16 15.9c0-0.8 0-1.6 0-2.4 0-3.1 0-6.2 0-9.4 0-1.2 0-2.3 0-3.5C16 0.2 16.2 0 15.7 0 13.1 0 10.6 0 8 0S2.9 0 0.3 0C-0.2 0 0 0.2 0 0.7c0 1.2 0 2.3 0 3.5C0 8.1 0 12.1 0 16z"/>
      <path class="stripe"
            fill="<?php print $pattern_color; ?>"
            d="M0 3.2c0.5-0.5 1.1-1.1 1.6-1.6C1.8 1.4 2.9 0 3.2 0 4.8 0 6.4 0 8 0 5.3 2.7 2.7 5.3 0 8 0 6.4 0 4.8 0 3.2zM3.2 16C7.5 11.7 11.7 7.5 16 3.2c0 0.9 0 1.7 0 2.6 0 0.4 0.2 2 0 2.2 -1.3 1.3-2.6 2.6-3.9 3.9 -1.3 1.3-2.7 2.7-4 4C7.8 16.2 6.4 16 6 16 5.1 16 4.1 16 3.2 16z"/>
    </g>
  </g>
</pattern>

svg.parents-logo.tpl.php

<g class="parents-logo" transform="translate(378, 509)">
  <path fill="<?php print $color; ?>" d="M143.205,30.684h0.075c0.979-1.975,3.392-3.172,5.953-3.172c3.466,0,5.199,1.622,5.199,5.358v9.067h1.669v1.155h-8.979
	v-1.155h2.186V31.601c0-2.397-1.131-2.544-2.262-2.544c-2.034,0-3.842,2.538-3.842,4.301v8.579h2.186v1.155h-9.24v-1.155h1.93
	V29.089h-1.93v-1.155h2.081c1.507,0,4.219,0,4.974-0.917V30.684L143.205,30.684z M168.752,32.967
	c-3.066-0.461-3.467-0.49-3.467-2.115c0-1.622,1.433-2.435,3.392-2.435c2.788,0,4.295,1.871,5.199,4.127h0.604l-0.227-4.935h-0.528
	c0,0.423-0.452,0.775-1.13,0.775c-1.205,0-2.185-0.775-3.843-0.775c-3.692,0-5.501,2.891-5.501,5.076
	c0,4.513,3.616,4.864,6.404,5.218c2.601,0.316,3.769,0.564,3.769,2.114c0,1.834-1.583,2.788-3.542,2.788
	c-2.863,0-5.124-2.506-5.803-5.044h-0.671v3.79c0,0.204-0.111,0.379-0.273,0.475c-0.003,0.001-0.073,0.036-0.101,0.046
	c-0.195,0.084-0.377,0.123-0.604,0.123c-0.904,0-1.356-0.564-1.356-1.763V28.729h2.573v-1.503h-2.573v-5.284l-1.471,0.002
	c0,0-0.127,1.599-0.723,2.745c-0.739,1.421-1.413,1.805-2.13,2.125c-0.838,0.235-0.733,0.41-2.76,0.41v1.503h2.33v10.999
	c0,3.243,2.643,3.877,3.698,3.877c1.413,0,2.564-0.312,3.389-0.955l0.575-0.466c0.554-0.498,0.929,0.09,1.604,0.41
	c0.874,0.415,2.262,1.021,4.296,1.021c3.466,0,5.728-2.82,5.728-5.641C175.609,33.743,172.595,33.531,168.752,32.967
	 M136.282,37.577c-0.829,3.243-2.938,5.044-5.803,5.044c-3.466,0-3.472-2.506-3.472-4.833v-2.89h10.173
	c-0.075-3.736-3.768-7.262-7.61-7.262c-2.299,0-4.484,1.022-5.954,2.679c-0.07,0.076-0.137,0.153-0.204,0.231
	c0.035-0.108,0.066-0.22,0.091-0.339v-0.282c0-1.622-1.281-2.608-3.014-2.608c-2.336,0-4.069,1.133-4.748,2.825l-0.075-0.173v-2.933
	c-0.754,0.916-3.542,0.916-5.049,0.916h-2.004v1.156h2.005v0.778c0,0.001,0,0.003,0,0.004l0,0c0-0.001,0-0.003,0-0.004v11.349v0.037
	l-0.003,0.001c-0.022,0.285-0.257,0.51-0.548,0.51h-1.191c-0.284,0-0.516-0.216-0.546-0.492l-0.001-0.002V41.27
	c-0.001-0.013-0.004-0.026-0.004-0.04v-0.421l-0.004-0.479v-8.111c0-3.877-3.836-4.846-7.618-4.918
	c-1.055-0.02-2.252,0.013-3.266,0.343c-0.829,0.282-1.447,0.595-1.912,0.916c0.564-0.993,0.842-2.116,0.817-3.296
	c-0.084-4.07-1.904-6.649-8.309-6.649H75.609v1.485h2.993v20.97v0.694h-2.993v1.329h11.793v-1.329h-3.074V32.37
	c1.055,0.07,1.142,0.106,2.799,0.07c3.187,0.021,5.59-0.887,7.143-2.319c-0.069,0.233-0.089,0.449-0.089,0.632
	c0,0.742,0.709,3.35,3.663,2.266c1.842-0.675,1.182-3.476-0.739-3.945c-0.146-0.035-0.252-1.133,3.121-1.133
	c1.199,0,2.916,0.097,3.018,2.097l0.125,4.115c-1.085,0.156-3.489,0.322-3.489,0.322c-3.391,0.281-7.008,1.128-7.008,4.724
	c0,2.961,2.864,4.159,5.727,4.159c1.733,0,3.843-0.493,4.823-2.115c0.678,1.48,1.174,1.845,3.25,1.845
	c0.761,0,2.44,0.005,2.44,0.005l8.858-0.006l-0.001-1.306h-2.227v-8.048c0-2.679,1.13-4.865,3.165-5.499l0.076,0.07
	c-0.678,0.635-0.829,1.128-0.829,1.974c0,1.339,1.131,2.256,2.713,2.256c0,0,0.891-0.04,1.655-0.656
	c-0.171,0.318-0.264,0.534-0.264,0.534c-0.49,1.022-0.75,2.169-0.75,3.403c0,4.441,3.466,7.755,8.138,7.755
	c4.898,0,6.857-2.608,7.687-5.993L136.282,37.577L136.282,37.577z M127.008,31.303c0-2.115,1.056-3.031,2.336-3.031
	c1.732,0,2.769,1.128,2.769,3.031v2.44h-5.104V31.303z M85.168,31.459c-0.602,0-0.653-0.017-0.84-0.141V19.751h2.196
	c3.994,0,4.562,1.344,4.562,5.715C91.087,29.485,89.539,31.459,85.168,31.459z M103.347,38.846c0,1.833-0.979,3.281-2.713,3.281
	c-2.486,0-2.562-1.941-2.562-3.704c0-1.903,0.226-2.894,2.562-3.14l2.713-0.196V38.846z"/>
</g>

The overall template that have all of the pieces

<svg xmlns="http://www.w3.org/2000/svg"
     width="564" height="564"
     viewBox="0 0 564 564"
     version="1.1"
     class="babynames-name-card">
  <title><?php print $name; ?></title>
  <desc>Generated by parents.com.</desc>
  <defs>
    <?php print render($pattern); ?>
  </defs>
  <rect x="0" y="0" width="564" height="564" fill="url(#pattern)"/>
  <circle cx="281.500638" cy="282.5" r="232.5" fill="#FFFFFF"/>
  <g class="babynames-dingbat-arrow" transform="translate(246.217391, 110.347826)"
     stroke-width="3" stroke="#FFD905">
    <g transform="translate(1.000000, 1.000000)">
      <g transform="translate(23.441323, 0.000000)">
        <path fill="none"
              stroke="#FFD905"
              stroke-width="3"
              d="M18.9 0.4c7.7 6.4 15.3 12.8 23 19.2 1.7 1.4 3.4 2.9 5.1 4.3 -7.7 6.4-15.3 12.8-23 19.2 -1.7 1.4-3.4 2.9-5.1 4.3M0.1 23.4h46.9"/>
      </g>
      <path fill="none"
            stroke="#FFD905"
            stroke-width="3"
            d="M9.5 9.7c4.7 4.7 9.4 9.4 14.1 14.1 -4.7 4.7-9.4 9.4-14.1 14.1M0.1 14.4c1.6 1.6 3.1 3.1 4.7 4.7 1.1 1.1 2.1 2.1 3.2 3.2 0.2 0.2 1.5 1.2 1.5 1.5s-1.3 1.3-1.5 1.5c-1.1 1.1-2.1 2.1-3.2 3.2 -1.6 1.6-3.1 3.1-4.7 4.7"/>
    </g>
  </g>
  <path d="M85.826087,327.365217 L478.173913,327.365217" stroke="#DEDEDE" stroke-linecap="square"></path>
  <text x="285" y="240" class="babynames-name"
        style="font-family:Open Sans; font-weight: bold; font-size: 48px; dominant-baseline: central; text-anchor: middle;"
        fill="<?php print $name_color; ?>"><?php print $name; ?>
  </text>
  <text x="281.500638" y="290" class="babynames-meaning"
        style="font-family:Open Sans; font-style: italic; font-size: 24px; dominant-baseline: central; text-anchor: middle;"
        fill="#000000"><?php print $meaning; ?>
  </text>
  <?php $line_position = 378; ?>
  <?php foreach ($name_info_lines as $line): ?>
    <text x="281.500638" y="<?php print $line_position; ?>" class="babynames-info-line1"
          style="font-family:Open Sans; font-size: 18px; dominant-baseline: central; text-anchor: middle;"
          fill="#999999"
          xml:space="preserve"><?php print $line; ?></text>
    <?php $line_position += 37; ?>
  <?php endforeach; ?>
  <?php print render($parents_logo); ?>
</svg>

The corresponding colors and how they are used:

const BABY_NAMES_NAME_CARD_COLORS = array(
  'Girl' => array(
    'color1' => '#8d79c5',
    'color2' => '#d1c9e8',
    'color3' => '#ebe6f4',
    'color4' => '#f6f4fa',
    'color5' => '#faf9fc',
  ),
  'Boy' => array(
    'color1' => '#7fc142',
    'color2' => '#cce6b3',
    'color3' => '#e2f2d5',
    'color4' => '#f6fbf2',
    'color5' => '#FCFDFB',
  ),
  'Either' => array(
    'color1' => '#ffaf05',
    'color2' => '#ffe182',
    'color3' => '#fff5bf',
    'color4' => '#fffbe3',
    'color5' => '#fffefa',
  )
);

...

  $pattern_render_array = [
    '#theme' => $pattern,
  ];
  switch ($pattern) {
    case 'baby_names_svg_pattern_geo':
      $pattern_render_array['#pattern_color1'] = $colors['color3'];
      $pattern_render_array['#pattern_color2'] = $colors['color5'];
      $pattern_render_array['#background_color'] = $colors['color4'];
      break;
    default:
      $pattern_render_array['#pattern_color'] = $colors['color3'];
      $pattern_render_array['#background_color'] = $colors['color4'];
      break;
  }
  return $pattern_render_array;

One note about the svg

The support for text wrapping in svg using foreignObject is not supported by the later conversion (or IE 11). Hence I cannot use that. Fortunately there are only two possibilities for the baby name info. They are either two lines or three. So I used a bit of logic inside the template to figure out how to generate the text tags.

I wish this was it

But pinterest mobile apps do not seem to support svg. Hence it becomes a necessity to convert the svg to a raster format that’s more friendly to pinterest. I chose to use imagick because that’s already available on our server. The version we use on the server is ImageMagick 6.7.2-7 2015-02-27 Q16. Trying to figure out how convert the svg to gif format actually took way longer time than figuring out the svg itself. Mainly the complication is because we are using a custom font on the graphic in its bold, italic and normal styles.

The tricky parts

First of all, you must convert svg using rsvg library as the native imagick svg conversion does not work well. When you run

convert -list configure | grep --color DELEGATES

Make sure you see “… rsvg …”.

Second, for the longest time I had in my svg

style="font-family: 'Open Sans';..." 

The single quote DOES NOT play well with imagick conversion. It took forever to figure out imagick wants it to look like:

style="font-family:Open Sans;..." 

instead.

The third part is trying to get the conversion to recognize the custom font Open Sans. We tried many different variations on our linux server. What ultimately worked was grabbing the ttf files from rhel7 rpm and then putting them in /usr/share/fonts and run fc-cache to make the system load the new font.

Finally, we have to enable the imagick php extension to use the its class directly. We have imagick module version 3.3.0 on the server.

With everything setup on the server, the actual code to do the conversion is very simple. You create a menu callback and the callback function looks like:

  $image_dir = variable_get('file_public_path') . '/baby-names/name-card/';
  $gif = $image_dir . $name_id . '.gif';

  if (file_exists($gif)) {
    header('Content-Type: image/gif');
    echo file_get_contents($gif);
  }
  else {
    $svg_content_render_array = [
      '#theme' => 'baby_names_name_card',
      '#name' => '...',
      '#meaning' => '...',
      '#gender' => '..',
      '#origins' => [...],
      '#popularity' => '...',
    ];

    $svg_content = render($svg_content_render_array);
    $svg = $image_dir . $name_id . '.svg';
    file_put_contents($svg, $svg_content);

    $image = new Imagick();
    $image->readImageBlob('<?xml version="1.0" encoding="UTF-8" standalone="no"?>' . $svg_content);
    $image->setImageFormat("gif");
    $image->writeImage($gif);
    header('Content-Type: image/gif');
    echo $image;
  }

I think being able to use svg to generate the dynamic graphics makes the code super straightforward. It took days to set up the server to allow imagick to properly convert the svg into gif with the correct font in their corresponding styles. However when it’s finally all working, it’s the slickest thing I’ve done for quite some time!

Leave a comment