mediawiki-extensions-ImageMap/ImageMap_body.php

328 lines
9.9 KiB
PHP

<?php
/**
* Image map extension.
*
* Syntax:
* <imagemap>
* Image:Foo.jpg | 100px | picture of a foo
*
* rect 0 0 50 50 [[Foo type A]]
* circle 50 50 20 [[Foo type B]]
*
* desc bottom-left
* </imagemap>
*
* Coordinates are relative to the source image, not the thumbnail
*
*/
require_once( dirname( __FILE__ ) . '/ImageMap.i18n.php' );
global $wgMessageCache;
foreach( efImageMapMessages() as $lang => $messages )
$wgMessageCache->addMessages( $messages, $lang );
class ImageMap {
static public $id = 0;
const TOP_RIGHT = 0;
const BOTTOM_RIGHT = 1;
const BOTTOM_LEFT = 2;
const TOP_LEFT = 3;
const NONE = 4;
static function render( $input, $params, $parser ) {
global $wgScriptPath, $wgUser, $wgImageMapAllowExternalLinks, $wgUrlProtocols, $wgNoFollowLinks;
$lines = explode( "\n", $input );
$first = true;
$lineNum = 0;
$output = '';
$links = array();
# Define canonical desc types to allow i18n of 'imagemap_desc_types'
$descTypesCanonical = 'top-right, bottom-right, bottom-left, top-left, none';
$descType = self::BOTTOM_RIGHT;
$defaultLinkAttribs = false;
$realmap = true;
foreach ( $lines as $line ) {
++$lineNum;
$externLink = false;
$line = trim( $line );
if ( $line == '' || $line[0] == '#' ) {
continue;
}
if ( $first ) {
$first = false;
# The first line should have an image specification on it
# Extract it and render the HTML
$bits = explode( '|', $line, 2 );
if ( count( $bits ) == 1 ) {
$image = $bits[0];
$options = '';
} else {
list( $image, $options ) = $bits;
}
$imageTitle = Title::newFromText( $image );
if ( !$imageTitle || $imageTitle->getNamespace() != NS_IMAGE ) {
return self::error( 'imagemap_no_image' );
}
$imageHTML = $parser->makeImage( $imageTitle, $options );
$domDoc = DOMDocument::loadXML( $imageHTML );
$xpath = new DOMXPath( $domDoc );
$imgs = $xpath->query( '//img' );
if ( !$imgs->length ) {
return self::error( 'imagemap_invalid_image' );
}
$imageNode = $imgs->item(0);
$thumbWidth = $imageNode->getAttribute('width');
$thumbHeight = $imageNode->getAttribute('height');
$imageObj = function_exists( 'wfFindFile' ) ? wfFindFile( $imageTitle ) : new Image( $imageTitle );
if ( !$imageObj || !$imageObj->exists() ) {
return self::error( 'imagemap_invalid_image' );
}
# Add the linear dimensions to avoid inaccuracy in the scale
# factor when one is much larger than the other
# (sx+sy)/(x+y) = s
$denominator = $imageObj->getWidth() + $imageObj->getHeight();
$numerator = $thumbWidth + $thumbHeight;
if ( $denominator <= 0 || $numerator <= 0 ) {
return self::error( 'imagemap_invalid_image' );
}
$scale = $numerator / $denominator;
continue;
}
# Handle desc spec
$cmd = strtok( $line, " \t" );
if ( $cmd == 'desc' ) {
$typesText = wfMsgForContent( 'imagemap_desc_types' );
if ( $descTypesCanonical != $typesText ) {
// i18n desc types exists
$typesText = $descTypesCanonical . ', ' . $typesText;
}
$types = array_map( 'trim', explode( ',', $typesText ) );
$type = trim( strtok( '' ) );
$descType = array_search( $type, $types );
if ( $descType > 4 ) {
// A localized descType is used. Subtract 5 to reach the canonical desc type.
$descType = $descType - 5;
}
if ( $descType === false || $descType < 0 ) { // <0? In theory never, but paranoia...
return self::error( 'imagemap_invalid_desc', $typesText );
}
continue;
}
# Find the link
$link = trim( strstr( $line, '[' ) );
if ( preg_match( '/^ \[\[ ([^|]*+) \| ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
$title = Title::newFromText( $m[1] );
$alt = trim( $m[2] );
} elseif ( preg_match( '/^ \[\[ ([^\]]*+) \]\] \w* $ /x', $link, $m ) ) {
$title = Title::newFromText( $m[1] );
if (is_null($title))
return self::error('imagemap_invalid_title', $lineNum);
$alt = $title->getFullText();
} elseif ( $wgImageMapAllowExternalLinks && ( in_array( substr( $link , 1 , strpos($link, '//' )+1 ) , $wgUrlProtocols ) || in_array( substr( $link , 1 , strpos($link, ':' ) ) , $wgUrlProtocols ) ) ) {
if ( preg_match( '/^ \[ ([^\s]*+) \s ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
$title = htmlspecialchars( $m[1] );
$alt = htmlspecialchars( trim( $m[2] ) );
$externLink = true;
} elseif ( preg_match( '/^ \[ ([^\]]*+) \] \w* $ /x', $link, $m ) ) {
$title = htmlspecialchars( $m[1] );
$alt = htmlspecialchars( trim( $m[1] ) );
$externLink = true;
}
} else {
return self::error( 'imagemap_no_link', $lineNum );
}
if ( !$title ) {
return self::error( 'imagemap_invalid_title', $lineNum );
}
$shapeSpec = substr( $line, 0, -strlen( $link ) );
# Tokenize shape spec
$shape = strtok( $shapeSpec, " \t" );
switch ( $shape ) {
case 'default':
$coords = array();
break;
case 'rect':
$coords = self::tokenizeCoords( 4, $lineNum );
if ( !is_array( $coords ) ) {
return $coords;
}
break;
case 'circle':
$coords = self::tokenizeCoords( 3, $lineNum );
if ( !is_array( $coords ) ) {
return $coords;
}
break;
case 'poly':
$coords = array();
$coord = strtok( " \t" );
while ( $coord !== false ) {
$coords[] = $coord;
$coord = strtok( " \t" );
}
if ( !count( $coords ) ) {
return self::error( 'imagemap_missing_coord', $lineNum );
}
break;
default:
return self::error( 'imagemap_unrecognised_shape', $lineNum );
}
# Scale the coords using the size of the source image
foreach ( $coords as $i => $c ) {
$coords[$i] = intval( round( $c * $scale ) );
}
# Construct the area tag
$attribs = array();
if ( $externLink ) {
$attribs['href'] = $title;
$attribs['class'] = 'plainlinks';
if ( $wgNoFollowLinks ) {
$attribs['rel'] = 'nofollow';
}
} else {
$attribs['href'] = $title->escapeLocalURL() . $title->getFragmentForURL();
}
if ( $shape != 'default' ) {
$attribs['shape'] = $shape;
}
if ( $coords ) {
$attribs['coords'] = implode( ',', $coords );
}
if ( $alt != '' ) {
if ( $shape != 'default' ) {
$attribs['alt'] = $alt;
}
$attribs['title'] = $alt;
}
if ( $shape == 'default' ) {
$defaultLinkAttribs = $attribs;
} else {
$output .= Xml::element( 'area', $attribs ) . "\n";
}
if ( $externLink ) {
$extLinks[] = $title;
} else {
$links[] = $title;
}
}
if ( $first ) {
return self::error( 'imagemap_no_image' );
}
if ( $output == '' && $defaultLinkAttribs == '' ) {
return self::error( 'imagemap_no_areas' );
} elseif ( $output == '' && $defaultLinkAttribs != '' ) {
// no areas defined, default only. It's not a real imagemap, so we do not need some tags
$realmap = false;
}
if ( $realmap ) {
# Construct the map
$mapName = "ImageMap_" . ++self::$id;
$output = "<map name=\"$mapName\">\n$output</map>\n";
# Alter the image tag
$imageNode->setAttribute( 'usemap', "#$mapName" );
}
# Add a surrounding div, remove the default link to the description page
$anchor = $imageNode->parentNode;
$parent = $anchor->parentNode;
$div = $parent->insertBefore( new DOMElement( 'div' ), $anchor );
$div->setAttribute( 'style', 'position: relative;' );
if ( $defaultLinkAttribs ) {
$defaultAnchor = $div->appendChild( new DOMElement( 'a' ) );
foreach ( $defaultLinkAttribs as $name => $value ) {
$defaultAnchor->setAttribute( $name, $value );
}
$imageParent = $defaultAnchor;
} else {
$imageParent = $div;
}
$imageParent->appendChild( $imageNode->cloneNode( true ) );
$parent->removeChild( $anchor );
# Determine whether a "magnify" link is present
$xpath = new DOMXPath( $domDoc );
$magnify = $xpath->query( '//div[@class="magnify"]' );
if ( !$magnify->length && $descType != self::NONE ) {
# Add image description link
if ( $descType == self::TOP_LEFT || $descType == self::BOTTOM_LEFT ) {
$descLeft = 0;
} else {
$descLeft = $thumbWidth - 20;
}
if ( $descType == self::TOP_LEFT || $descType == self::TOP_RIGHT ) {
$descTop = 0;
} else {
$descTop = $thumbHeight - 20;
}
$descAnchor = $div->appendChild( new DOMElement( 'a' ) );
$descAnchor->setAttribute( 'href', $imageTitle->escapeLocalURL() );
$descAnchor->setAttribute( 'title', wfMsgForContent( 'imagemap_description' ) );
$descAnchor->setAttribute( 'style', "position:absolute; top: {$descTop}px; left: {$descLeft}px;" );
$descImg = $descAnchor->appendChild( new DOMElement( 'img' ) );
$descImg->setAttribute( 'alt', wfMsgForContent( 'imagemap_description' ) );
$descImg->setAttribute( 'src', "$wgScriptPath/extensions/ImageMap/desc-20.png" );
$descImg->setAttribute( 'style', 'border: none;' );
}
# Output the result
# We use saveXML() not saveHTML() because then we get XHTML-compliant output.
# The disadvantage is that we have to strip out the DTD
$output .= preg_replace( '/<\?xml[^?]*\?>/', '', $domDoc->saveXML() );
# Register links
$parser->mOutput->addImage( $imageTitle->getDBkey() );
foreach ( $links as $title ) {
$parser->mOutput->addLink( $title );
}
if ( isset( $extLinks ) ) {
foreach ( $extLinks as $title ) {
$parser->mOutput->addExternalLink( $title );
}
}
# Armour output against broken parser
$output = str_replace( "\n", '', $output );
return $output;
}
static function tokenizeCoords( $count, $lineNum ) {
$coords = array();
for ( $i = 0; $i < $count; $i++ ) {
$coord = strtok( " \t" );
if ( $coord === false ) {
return self::error( 'imagemap_missing_coord', $lineNum );
}
if ( !is_numeric( $coord ) || $coord > 1e9 || $coord < 0 ) {
return self::error( 'imagemap_invalid_coord', $lineNum );
}
$coords[$i] = $coord;
}
return $coords;
}
static function error( $name, $line = false ) {
return wfMsgForContent( $name, $line );
}
}
?>