Version 6 (modified by guest, 11 months ago)

fixes to tilestitching PHP

Tile Stitching is a process for rendering an OpenLayers map to a static image, commonly called "printing" This static image could be saved to disk, displayed with a common <img> tag, inserted into a PDF or other document, ...

This method involves the OpenLayers Map composing a list of the visible tiles and sending them to a server-side program. The server-side program downloads the tiles' images and composes them into a finished image. Thus, a tile stitching approach consists of three parts:

  • A client component to prepare a list of tile information
  • A server-side program to fetch the tile images and composite them into a final output
  • A client callback to do something useful with the resulting image

Client-side: Tile collection

Update March 2012: This is available as an OpenLayers Control at  http://www.mapsportal.org/olprint/OpenLayers_Control_TileStitchPrinter.js The OpenLayers Control version does not require the json2.js library. For more information, see the working example and documentation at  http://www.mapsportal.org/olprint/

The client-side JavaScript uses the JSON JavaScript library, for encoding the collected tiles in JSON format for easy consumption by PHP. The JSON library can be obtained from  http://www.JSON.org/js.html

// this assumes that the Map object is a JavaScript variable named "map"
var print_wait_win = null;
function PrintMap() {
    //-- post a wait message
    alert("One moment please");

    // go through all layers, and collect a list of objects
    // each object is a tile's URL and the tile's pixel location relative to the viewport
    var offsetX = parseInt(map.layerContainerDiv.style.left);
    var offsetY = parseInt(map.layerContainerDiv.style.top);
    var size  = map.getSize();
    var tiles = [];
    for (layername in map.layers) {
        // if the layer isn't visible at this range, or is turned off, skip it
        var layer = map.layers[layername];
        if (!layer.getVisibility()) continue;
        if (!layer.calculateInRange()) continue;
        // iterate through their grid's tiles, collecting each tile's extent and pixel location at this moment
        for (tilerow in layer.grid) {
            for (tilei in layer.grid[tilerow]) {
                var tile     = layer.grid[tilerow][tilei]
                var url      = layer.getURL(tile.bounds);
                var position = tile.position;
                var tilexpos = position.x + offsetX;
                var tileypos = position.y + offsetY;
                var opacity  = layer.opacity ? parseInt(100*layer.opacity) : 100;
                tiles[tiles.length] = {url:url, x:tilexpos, y:tileypos, opacity:opacity};
            }
        }
    }

    // hand off the list to our server-side script, which will do the heavy lifting
    var tiles_json = JSON.stringify(tiles);
    var printparams = 'width='+size.w + '&height='+size.h + '&tiles='+escape(tiles_json) ;
    OpenLayers.Request.POST(
      { url:'lib/print.php',
        data:OpenLayers.Util.getParameterString({width:size.w,height:size.h,tiles:tiles_json}),
        headers:{'Content-Type':'application/x-www-form-urlencoded'},
        callback: function(request) {
           window.open(request.responseText);
        }
      }
    );
}

Server side: PHP

This is the basic printing process implemented in PHP. It iterates over the submitted tiles, pasting them onto a canvas.

Requirements: PHP has GD support (your OS may have the GD module as a separate package). PHP 5.2.0 or later for the json_decode() function.

  <?php
  $TEMP_DIR = '/var/www/htdocs/tmp/';
  $TEMP_URL = '/tmp/';

  function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $opacity){
      $w = imagesx($src_im);
      $h = imagesy($src_im);
      $cut = imagecreatetruecolor($src_w, $src_h);
      imagecopymerge($dst_im, $cut, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $opacity);
  }

  // fetch the request params, and generate the name of the tempfile and its URL
  $width    = @$_REQUEST['width'];  if (!$width) $width = 1024;
  $height   = @$_REQUEST['height']; if (!$height) $height = 768;
  $tiles    = json_decode(@$_REQUEST['tiles']);
  //$tiles    = json_decode(stripslashes(@$_REQUEST['tiles'])); // use this if you use magic_quotes_gpc
  $random   = md5(microtime().mt_rand());
  $file     = sprintf("%s/%s.jpg", $TEMP_DIR, $random );
  $url      = sprintf("%s/%s.jpg", $TEMP_URL, $random );

  // lay down an image canvas
  // Notice: in MapServer if you have set a background color
  // (eg. IMAGECOLOR 60 100 145) that color is your transparent value
  // $transparent = imagecolorallocatealpha($image,60,100,145,127);
  $image = imagecreatetruecolor($width,$height);
  imagefill($image,0,0, imagecolorallocate($image,255,255,255) ); // fill with white

  // loop through the tiles, blitting each one onto the canvas
  foreach ($tiles as $tile) {
     // try to convert relative URLs into full URLs
     // this could probably use some improvement
     $tile->url = urldecode($tile->url);
     if (substr($tile->url,0,4)!=='http') {
        $tile->url = preg_replace('/^\.\//',dirname($_SERVER['REQUEST_URI']).'/',$tile->url);
        $tile->url = preg_replace('/^\.\.\//',dirname($_SERVER['REQUEST_URI']).'/../',$tile->url);
        $tile->url = sprintf("%s://%s:%d/%s", isset($_SERVER['HTTPS'])?'https':'http', $_SERVER['SERVER_ADDR'], $_SERVER['SERVER_PORT'], $tile->url);
     }
     $tile->url = str_replace(' ','+',$tile->url);

     // fetch the tile into a temp file, and analyze its type; bail if it's invalid
     $tempfile =  sprintf("%s/%s.img", $TEMP_DIR, md5(microtime().mt_rand()) );
     file_put_contents($tempfile,file_get_contents($tile->url));
     list($tilewidth,$tileheight,$tileformat) = @getimagesize($tempfile);
     if (!$tileformat) continue;

     // load the tempfile's image, and blit it onto the canvas
     switch ($tileformat) {
        case IMAGETYPE_GIF:
           $tileimage = imagecreatefromgif($tempfile);
           break;
        case IMAGETYPE_JPEG:
           $tileimage = imagecreatefromjpeg($tempfile);
           break;
        case IMAGETYPE_PNG:
           $tileimage = imagecreatefrompng($tempfile);
           break;
     }
       imagecopymerge_alpha($image, $tileimage, $tile->x, $tile->y, 0, 0, $tilewidth, $tileheight, $tile->opacity);
  }

  // save to disk and tell the client where they can pick it up
  imagejpeg($image,$file,100);
  print $url;
  ?>

Server side: Python

This version of the server-side print service uses the Python Imaging Library (PIL) for the image blending, and uses threading to do multiple image downloads in parallel. This is significantly faster than the non-threaded PHP version.

This version allows for the &image=1 parameter, which outputs the generated image directly to the browser. This could be useful for other server-side programs which would consume that image into a PDF or other document.

#!/bin/env python
# a Python-based version of the OpenLayers tile-stitching process

TEMP_DIR = "/maps/images.tmp/"
TEMP_URL = "http://www.mapsportal.org/images.tmp/"

##################################################################################

import cgi, cgitb, urllib, simplejson
from PIL import Image
import sys, tempfile, os.path, threading
from time import sleep

"""
The OLTileStitchServer accepts a dictionary of configuration parameters,
blits all of the specified tiles together into a single canvas,
and either outputs the generated image or returns the URL of that image.

width - the width of the image and of the OL Map viewport
height - the height of the image and of the OL Map viewport
tiles - the JSON-encoded tiles as generated by print.js
image - 1 to request the image directly to the browser; default behavior is to output the URL of the resulting image

The default behavior of returning an image URL is meant for use with AJAX techniques,
in which returning image data would be inappropriate. Instead the URL of the image
is returned, and the browser may open it in a new window, assign it to an IMG.src attribute, etc.
"""
class OLTileStitchServer:
    def __init__(self,params):
        self.width   = params['width']  # the width of the image to generate
        self.height  = params['height'] # the height of the image to generate
        self.tiles   = params['tiles']  # the decoded set of JSON tiles
        self.directoutput = params.get('image',False)
        self.outfilename  = None # the filename of the generated final image
        self.outfileurl   = None # the URL of the generated final image

        # create a blank canvas object
        self.canvas = Image.new('RGB', (self.width,self.height), (255,255,255) )
        self.canvas.lock = threading.Lock()

        # a semaphore to keep us from running a zillion threads at once
        self.thread_pool = threading.Semaphore(10)

        # load the tiles, then render the final product
        self.load()
        self.render()

    def load(self):
        # start a new thread for each tile...
        for tile in self.tiles:
            run_thread = threading.Thread(target=self.add_tile_to_canvas,args=(tile,))
            run_thread.start()

        # ... and don't return until the last thread returns
        while threading.activeCount() > 1:
            sleep(1)

    def add_tile_to_canvas(self,tilespec):
        self.thread_pool.acquire()
        # A tile is:
        # { url:URL, x:integer pixel position, y:integer pixel position, opacity:integer 0-100 }
        try:
            (tempfilename,headers) = urllib.urlretrieve(tilespec['url'])
            image = Image.open(tempfilename)

            # if it's a GIF with transparency, convert to pure RGBA
            if image.mode == 'P' and image.info.has_key('transparency'):
                image = image.convert('RGBA')

            # lock the canvas, paste the images together, unlock
            self.canvas.lock.acquire()
            if image.mode == 'RGBA':
                self.canvas.paste(image, (tilespec['x'],tilespec['y']), image )
            else:
                self.canvas.paste(image, (tilespec['x'],tilespec['y']) )
            self.canvas.lock.release()
        except:
            pass
        self.thread_pool.release()

    def render(self):
        (outfilehandle,outfilename) = tempfile.mkstemp('.jpg', 'print', TEMP_DIR)
        outfileurl = TEMP_URL + os.path.basename(outfilename)
        self.canvas.save(outfilename, 'jpeg')
        self.outfileurl  = outfileurl
        self.outfilename = outfilename
        return(self.outfileurl,self.outfilename)

    def serve(self):
        if self.directoutput:
            print "Content-type: image/jpeg"
            print "Content-disposition: inline; filename=%s" % os.path.basename(self.outfilename)
            print
            f = open(self.outfilename,'r')
            while True:
                chunk = f.read(1048576)
                if not chunk:
                    break
                sys.stdout.write(chunk)
            f.close()
        else:
            print "Content-type: text/plain"
            print
            print self.outfileurl


"""
If this module is executed, which it often will be as a CGI program,
load the settings from the CGI parameters,
use the OLTileStitchServer subclass,
and have it automatically serve its finished product
"""
if __name__ == '__main__':
    cgitb.enable()

    # load up the settings and bail if anything's amiss
    params = cgi.FieldStorage()
    try:
        settings = {}
        settings['width']   = int( params.getvalue("width") )
        settings['height']  = int( params.getvalue("height") )
        settings['tiles']   = simplejson.loads( params.getvalue("tiles") )
        settings['image']   = bool(int( params.getvalue("image",0) ))
    except:
        print "Content-type: text/plain"
        print
        print "Invalid params: width=integer  height=integer  tiles=json_array"
        sys.exit(1)

    # load the OLTileStitcher subclass and let it do its job
    OLTileStitchServer(settings).serve()