Get the Bounding Box of an SVG Path

There may be times you need to find the bounding box of an SVG path. One way to find it would be to parse the path and then apply any group transformation on it to find the range of X and Y coordinates the object occupies. However that’s a bit of code, and I’m quite a bit lazy, so I decided to try another approach: rasterize the path object by itself and find where in the image it was drawn.

To do this, basically, all you need to do is erase all the other content from the document, leaving only the path you want to find the bounding box of and then trim the sides off with ImageMagick and have it tell you the info instead of generating an image.

Here’s an example ImageMagick convert command line:

bash:~>convert -density 200 pumpkinsheet.svg -trim info:-
pumpkinsheet.svg SVG 1813x2244 2066x2922+207+576 16-bit DirectClass 0.070u 0:00.029

First you have the file name, type, resulting crop size, original canvas size and the offsets where the crop starts. Respectively: pumpkinsheet.svg; SVG; 1813 x 2244; 2066×2922; and 207,586.

You can then find the bounding offsets of the drawn object:

Top left X: 207 (horizontal offset)
Top left Y: 576 (vertical offset)
Bottom right X: 207 + 1813 (horizontal offset + cropped width)
Bottom right Y: 576 + 2244 (vertical offset + cropped height)

Unless you use the default density (72) these canvas sizes won’t match up to what you likely have in your SVG set as the width and height, so convert them to percentages by dividing them by the original width (for the Xs) and the original height (for the Ys).

Once you have the percentages, multiply the width and height of your SVG if you need it in absolute points of the SVG instead of percentages.

A couple of caveats with this method:

  • If you have borders, they will add to the bounding box.
  • This will clip any path that falls outside the canvas unless you expand the canvas of the single-node object.
  • You’ll need to preserve patterns and gradients if you want those patterns and gradients to play a role in your bounding calculation.
  • Using higher densities will get you more sub-pixel accurate results, but will consume more memory in the process.

I have some rough code I’m using in my own project below. I just use the percentages for my purposes, as such the discover_bounding_box only returns the bounding coordinates as percentages. The create_single_object_svg function could function differently as well. Instead of making a copy of the SVG, it could just loop through the SVG’s DOM and remove any node that would result in drawing except our desired object. This would make it fairly easy to preserve patterns and gradients. It uses proc_open and DO NOT run this on a server with autoregister globals on. That is a recipe for disaster. The code below is provided for demonstration purposes only and should not necessarily be expected to work as you expect without modification.

= 0.1 && $precision <= 3)
   {
       $density = floor(72*$precision);
   }
    
   $singlesvg = create_single_object_svg($svg, $object);
   
  
   //Now that we've got the SVG in hand, let's pass it on to process image.
   ob_start();
   process_image($singlesvg, '-trim', 'svg', 'info', "-density $density");
   $info = trim(ob_get_clean());
   
   //Tokenize the fragments of text from our friend, ImageMagick.
   $fragments = explode(' ', $info);
   
   list($trimmed_width, $trimmed_height) = explode('x', $fragments[2]);
   //If both intval'd are not 0, then we can assume that all when well.
   if( ($trimmed_width = intval($trimmed_width)) > 0 && ($trimmed_height = intval($trimmed_height)) > 0)
   {
      //Now get the original width, original height, clip offsetx and clip offsety
      list($original_width, $original_height, $clip_offsetx, $clip_offsety) = 
         array_map('_intval', explode('+', str_replace('x', '+', $fragments[3])));
      
      $bounding_top_left_x = $clip_offsetx/$original_width;
      $bounding_top_left_y = $clip_offsety/$original_height;
      $bounding_bottom_right_x = ($clip_offsetx+$trimmed_width)/$original_width;
      $bounding_bottom_right_y = ($clip_offsety+$trimmed_height)/$original_height;
      
      return array($bounding_top_left_x, $bounding_top_left_y, 
         $bounding_bottom_right_x, $bounding_bottom_right_y);
   }
   
   return false;
}


/**
 * Creates an extract from an SVG based on an input DOM and desired object.
 * @param DOMDocument $svg A fully loaded DOMDocument for the SVG.
 * @param DOMNode $object Target node to extract.
 * @return String Returns an SVG file containing only the desired object.  At present
 * this function also removes patterns and gradients which may be a notable problem in 
 * cases where you want to find the visible bounding box of an object, not necessarily
 * it's exact box.
 */
function create_single_object_svg($svg, $object)
{
   //Get the width and height of the current document.
   $root = $svg->getElementsByTagName('svg')->item(0);
   $width = htmlspecialchars($root->getAttribute('width'));
   $height = htmlspecialchars($root->getAttribute('height'));
   
   //It's best if you have a solid color set in the style or fill attribute
   //but it shouldn't be that big a deal.   
   
   //Get an transforms from group nodes.
   $transforms = array();
   $current = $object;
   while($current->parentNode) //Continue climbing until there's no where else to climb to.
   {
      $current = $current->parentNode;
      if($current->nodeName == 'g')
      {
         $transforms[] = $current->getAttribute('transform');
      }
   }

   //Get the object node's xml.
   $objectxml = $svg->saveXML($object);

   //Make the group transform tags.
   $groupstarts = '';
   $groupends = '';


   //Reverse them so I put them in the right order.
   foreach(array_reverse($transforms) as $transform)
   {
      $stransform = trim(htmlspecialchars($transform));
      if($stransform != '')
      {
         $groupstarts .= "";
         $groupends .= '';
      }
   }

   return "xmlEncoding}\" standalone=\"no\"?>
    
       $groupstarts
       $objectxml
       $groupends
    ";
}

/**
 * Run stuff through image_magick uses the global convert path settings.  Outputs image on success (and sets header)
 * @param String $imgdata Raw image file data to pipe to image magick.
 * @param String $processing_instruction Command line options to pass to image magick's convert program.
 * @param String $inputtype Default: jpg Input image format.
 * @param String $outputtype Default: jpg Output image format.
 * @param String $preload_instructions Add instructions that go before the file to load.
 * @return void
 */
function process_image($imgdata, $processing_instruction, $inputtype = 'jpg', $outputtype = 'jpg', $preload_instructions = '')
{
   global $convertpath;
   
   if(function_exists('proc_open'))
   {
      //Let's just pipe to imagemagick to handle our graphics transform shall we?
      $descriptors = array(
         '0' => array("pipe", 'r'),
         '1' => array("pipe", 'w'),
         '2' => array("file", "/dev/null", "w")
      );
      
      $pipes = array();
      $process = proc_open($command = "$convertpath $preload_instructions {$inputtype}:- $processing_instruction {$outputtype}:-", $descriptors, $pipes, NULL, NULL, array('binary_pipes' => true));
      //die($command);
      if($process)
      {
         fwrite($pipes[0], $imgdata);
         //fwrite($pipes[0], EOF);
         fclose($pipes[0]);
         $contents = stream_get_contents($pipes[1]);
         proc_close($process);
         
         if(strlen($contents) > 50 || $outputtype == 'info')
         {
            echo $contents;
         }
         else
         {
            header('HTTP/1.1 500 Internal Error');
            header('content-type: text/plain');
            echo 'Failed to process image.';
         }
      }
      else
      {
         //Poor process died.  Should we do something?
         header('HTTP/1.1 500 Internal Error');
         header('content-type: text/plain');
         echo 'Failed to process image.';               
      }
   }
}

?>

7 Responses to “Get the Bounding Box of an SVG Path”

  1. epb

    Thanks I didn’t know about that. Though I can’t seem to find a PHP implementation of that interface anywhere. Do you know of one? Would be very handy to have. However, there do seem to be implementations for other languages. I noticed ones for JavaScript and Java in my Google searches.

  2. epb

    Thanks for the heads up, that’s really rather handy if you have permissions to install software on your server!

  3. Justin Souter

    There’s some code missing from the create_single_object_svg function.

  4. epb

    I know it’s a bit late, but thanks for pointing that out! Had to go retrieve it from the repo.

  5. Nathan

    Here’s a solution using ImageMagick:


    function getTrimmedSvg( $filePath )
    {
    $image = new Imagick();
    $image->readImage( $filePath );
    $image->trimImage( 0 );

    $imagePage = $image->getImagePage();
    $dimensions = $image->getImageGeometry();

    $minXOut = $imagePage['x'];
    $minYOut = $imagePage['y'];
    $widthOut = $dimensions["width"];
    $heightOut = $dimensions["height"];

    $xml = simplexml_load_file( $filePath );

    $xml["viewBox"] = "$minXOut $minYOut $widthOut $heightOut";

    return $xml->asXML();
    }

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>