Extension:LilyPond

From MediaWiki.org

Jump to: navigation, search

         

Manual on MediaWiki Extensions
List of MediaWiki Extensions
Crystal Clear action run.png
LilyPond

Release status: beta

Lilytest1.png
Implementation  Tag
Description This extension adds the option to typeset music
Author(s)  Johannes E. Schindelin
MediaWiki  1.6+
License No license specified
Download See Extension:LilyPond for latest version

check usage (experimental)

Contents

[edit] What can this extension do?

This extension allows editors to collaboratively typeset music using LilyPond notation.

[edit] Usage

LilyPond notation can be embedded in wikitext, by enclosing it within <lilypond>...</lilypond> tags. For example, this code:

<lilypond>\relative c' { f d f a d f e d cis a cis e a g f e }</lilypond>

will yield this image:

Lilytest1.png

If the notation is enclosed within <lilymidi>...</lilymidi> tags, the image will be clickable, and the link will download the MIDI file. Users' browsers may have to be configured to play MIDI files by means of plug-ins or external applications.

Another option is to use <lilybook>...</lilybook> tags. This allows full control over Lilypond settings, and it creates a full page of music. For example, this code:

<lilybook>
\version "2.12.2"

\header { 
 title = "Mary Had a Little Lamb"
 tagline = ##f
}

\paper {
 ragged-right = ##t
 ragged-bottom = ##t
 indent = 0\mm
}

melody = \relative c' {
 e d c d | e e e e |
 d d e d | c1 |
}
    
text = \lyricmode {
\set stanza = "1." Ma- ry had a lit- tle lamb,
 its fleece was white as snow.
}
    
\book{
  \score{ <<
    \new Voice = "one" { \melody }
    \new Lyrics \lyricsto "one" \text
    >>
 
    \layout { }
    \midi { }
  }
 
  \markup{
    \wordwrap-string #"
    Verse 2.
  
    All the children laughed and played,
    
    To see a lamb at school."
  }
}
</lilybook>

will yield this image:

Lilytest2.png

Lilypond is very advanced and powerful. See the Lilypond documentation for more examples and a complete user's guide. The only restriction is that the notation will need to be compatible with Lilypond's safe mode.

[edit] Setup

A one-off setup is needed by the wiki administrator.

No plug-ins or browser tweaks are needed in order to view and edit music notation. However, browser configuration may be necessary in order to listen to the MIDI files.

[edit] Prerequisites

  • PHP version 4.3.0. For earlier versions it will be necessary to install the GD library.
  • If not already present on your system, install LilyPond.

[edit] Installation

Copy the code in the "Code" section in a file called LilyPond.php, into the extensions directory.

[edit] Parameters

None.

See optional user settings in the "Code" section.

[edit] Changes to LocalSettings.php

Add the following line at the bottom of your LocalSettings.php file.

require_once("$IP/extensions/LilyPond.php");

You can also define optional parameters before this line, as described in the "Code" section.

[edit] Why is this not available in Wikipedia?

You can install and use this extension in your wiki, but it is currently not available in Wikipedia. Many people would love to use this extension in Wikipedia and other large-footprint wikis that they have no control over. However, in its current form, this is unfortunately impossible due to security concerns.

This extension is using Lilypond's safe mode, which according to the documentation "does not detect resource overuse. It is still possible to make the program hang indefinitely, for example by feeding cyclic data structures into the backend. Therefore, if using LilyPond on a publicly accessible webserver, the process should be limited in both CPU and memory usage." This extension is therefore not used in Wikipedia because it is considered a hazardous denial-of-service vector.

One way forward could be to use the Lilypond's jail mode instead, which requires a more involved set-up.

Another promising possibility is to use wikitex and mod_tex, which would extend collaborative editing to other fields of human knowledge. Their development, however, seems to have stalled. See also Extension:WikiTeX.

[edit] Credits

The first version of the trimming function was produced by Dan Williams and Vanessa X.

[edit] Code

<?php
 
/*
 
	MediaWiki extension: LilyPond
	=============================
 
 
To activate, edit your LocalSettings.php, add
 
	require_once("$IP/extensions/LilyPond.php");
 
and make sure that the images/ directory is writable.
 
 
Example wiki code: <lilypond>\relative c' { c d e f g }</lilypond>
 
If you want to typeset a fragment with clickable midi, use
 
	<lilymidi>...</lilymidi>
 
If you want write a complete lilypond file, use
 
	<lilybook>...</lilybook>
 
 
Tested with Lilypond version 2.12.2.
 
*/
 
# User Settings

# The following variables can be set in LocalSettings.php
# before the line:
# require_once("$IP/extensions/LilyPond.php");

# You can set the variable $wgLilypond if you want/need to override the
# path to the Lilypond executable. For example:
# $wgLilypond = "/home/username/bin/lilypond";

# Add a text link to prompt user to listen to midi, before and/or after
# the image. Remember line breaks
# $wgLilypondPreMidi  = "Listen<br>";
# $wgLilypondPostMidi = "<br>Listen";  

# If you want to avoid trimming the resulting image, set $wgLilypondTrim
# to false.
# $wgLilypondTrim = false;

# You can put a white border around the image if you like.
# $wgLilypondBorderX = 10;
# $wgLilypondBorderY = 0;

# End User Settings

# Defaulting of user settings
if( !isset( $wgLilypond ) )
	$wgLilypond = "PATH=\$PATH:/usr/local/bin /usr/local/bin/lilypond";
 
if( !isset( $wgLilypondPreMidi ) )
	$wgLilypondPreMidi = "";
 
if( !isset( $wgLilypondPostMidi ) )
	$wgLilypondPostMidi = "";
 
if( !isset( $wgLilypondTrim ) ) {
	$wgLilypondTrim = true;
}
 
if( !isset( $wgLilypondBorderX ) ) {
	$wgLilypondBorderX = 0;
}
 
if( !isset( $wgLilypondBorderY ) ) {
	$wgLilypondBorderY = 0;
}
 
$wgExtensionFunctions[] = "wfLilyPondExtension";
 
function wfLilyPondExtension() {
	global $wgParser;
	$wgParser->setHook( "lilypond", "renderLilyPondFragment" );
	$wgParser->setHook( "lilymidi", "renderLilyPondMidiFragment" );
	$wgParser->setHook( "lilybook", "renderLilyPond" );
}
 
function renderLilyPondMidiFragment( $lilypond_code )
{
	return renderLilyPondFragment( $lilypond_code, true );
}
 
function renderLilyPondFragment( $lilypond_code, $midi=false )
{
	return renderLilyPond( "\\header {\n"
			. "\ttagline = ##f\n"
			. "}\n"
			. "\\paper {\n"
			. "\traggedright = ##t\n"
			. "\traggedbottom = ##t\n"
			. "\tindent = 0\mm\n"
			. "}\n"
			. "\\score {\n"
			. $lilypond_code
			. "\t\\layout { }\n"
			. ($midi?"\t\\midi { }\n":"")
			. "}\n", $lilypond_code );
}
 
function renderLilyPond( $lilypond_code, $short_code=false )
{
	global $wgMathPath, $wgMathDirectory, $wgTmpDirectory, $wgLilypond, $wgLilypondPreMidi, $wgLilypondPostMidii, $wgLilypondTrim, $wgLilypondBorderX, $wgLilypondBorderY;
 
	$mf   = wfMsg( "math_failure" );
	$munk = wfMsg( "math_unknown_error" );
 
	$fname = "renderMusic";
 
	$md5 = md5($lilypond_code);
 
	if( file_exists( $wgMathDirectory."/".$md5.".midi" ) ) {
                $pre = "<a href=\"".$wgMathPath."/".$md5.".midi\"> " . $wgLilypondPreMidi;
                $post = $wgLilypondPostMidi . " </a>";
	} else {
		$pre = "";
		$post = "";
	}
 
	# if short_code is supplied, this is a fragment
	if( $short_code ) {
		$link = "<img src=\"".$wgMathPath."/".$md5.".png\" alt=\""
			.htmlspecialchars( $short_code )."\">";
 
		if( file_exists( "$wgMathDirectory/$md5.png" ) ) {
			return $pre.$link.$post;
		}
	} else {
		if(  file_exists( "$wgMathDirectory/$md5-1.png" ) ) {
			$link="";
			for($i=1; file_exists( $wgMathDirectory . "/" .
						$md5 . "-" . $i . ".png" );
					$i++) {
 
				$link .= "<img src=\"" . $wgMathPath . "/" .
					$md5 . "-" . $i . ".png\" alt=\"" .
					htmlspecialchars( "page ".$i )."\">";
			}
			return $pre.$link.$post;
		}
	}
 
	# Ensure that the temp and output dirs are available before continuing.
	if( !file_exists( $wgMathDirectory ) ) {
		if( !@mkdir( $wgMathDirectory ) ) {
			return "<b>$mf (" . wfMsg( "math_bad_output" ) .
				$wgMathDirectory . ")</b>";
		}
	} elseif( !is_dir( $wgMathDirectory ) ||
			!is_writable( $wgMathDirectory ) ) {
		return "<b>$mf (" . wfMsg( "math_bad_output" ) . ")</b>";
	}
	if( !file_exists( $wgTmpDirectory ) ) {
		if( !@mkdir( $wgTmpDirectory ) ) {
			return "<b>$mf (" . wfMsg( "math_bad_tmpdir" )
				. ")</b>";
		}
	} elseif( !is_dir( $wgTmpDirectory ) ||
			!is_writable( $wgTmpDirectory ) ) {
		return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";
	}
 
	$lyFile = $md5.".ly";
	$out = fopen( $wgTmpDirectory."/".$lyFile, "w" );
	if( $out === false ) {
		return "<b>$mf (" . wfMsg( "math_bad_tmpdir" ) . ")</b>";
	}
	fwrite( $out, $lilypond_code );
	fclose( $out );
 
	$cmd = $wgLilypond .
		" -dsafe='#t' -dbackend=eps --png --header=texidoc " .
		escapeshellarg($lyFile) . " 2>&1";
 
	wfDebug( "Lilypond: $cmd\n" );
	$oldcwd = getcwd();
	chdir( $wgTmpDirectory );
	$contents = exec( $cmd, $output, $ret );
	chdir( $oldcwd );
 
	if( $ret != 0 ) {
		return "<br><b>LilyPond error:</b><br><i>"
		. str_replace( array( $md5, " " ),
			array( "<b>your code</b>", "&nbsp;" ),
			nl2br( htmlentities( join( "\n", $output ) ) ) )
		. "</i><br>";
	}
 
	if($short_code) {
		$outputFile = $wgTmpDirectory."/".$md5.".png";
 
		if( !file_exists( $outputFile ) ) {
			return "<b>$mf (" . wfMsg( "math_image_error" )
				. ")</b>";
		}
 
		rename( $outputFile, $wgMathDirectory."/".$md5.".png");
	}
 
	# remove all temporary files
	$files = opendir( $wgTmpDirectory );
	$last_page = 0;
 
	while( false !== ($file = readdir( $files ))) {
		if( substr( $file, 0, 32 ) != $md5 )
			continue;
 
		$file_absolute = $wgTmpDirectory . "/" . $file;
		if( !$short_code && preg_match( '/-page(\d+)\.png$/',
					$file, $matches ) ) {
			if($matches[1]>$last_page)
				$last_page = $matches[1];
			rename( $file_absolute, $wgMathDirectory . "/" .
					$md5 . "-" . $matches[1] . ".png" );
			continue;
		}
 
                if( preg_match( '/.png$/', $file ) ) {
			rename( $file_absolute, $wgMathDirectory."/".$md5.".png" );
			continue;
                }
 
		if( preg_match( '/.midi$/', $file ) ) {
			rename( $file_absolute, $wgMathDirectory . "/" .
					$md5 . ".midi" );
                        $pre = "<a href=\"".$wgMathPath."/".$md5.".midi\"> " . $wgLilypondPreMidi;
                        $post = $wgLilypondPostMidi . " </a>";
			continue;
		}
 
		if( !is_file( $file_absolute ) )
			continue;
		unlink( $file_absolute );
	}
	closedir( $files );
 
	if( $short_code ) {
		if( !file_exists( $wgMathDirectory."/".$md5.".png" ) ) {
			$errmsg = wfMsg( "math_image_error" );
			return "<h3>$mf ($errmsg): " .
				htmlspecialchars($lilypond_code) . "</h3>";
		}
	} else {
                $link .= "<img src=\"".$wgMathPath."/".$md5.".png\" alt=\""
                . htmlspecialchars( "page " )."\">";
	}
 
	if( $wgLilypondTrim ) {
		$imgFile = $wgMathDirectory ."/" .$md5 . ".png";
		trimImage( $imgFile, $imgFile, 0xFFFFFF );
	};
 
	if( $wgLilypondBorderX > 0 || $wgLilypondBorderY > 0 ) {
                $imgFile = $wgMathDirectory ."/" .$md5 . ".png";
                frameImage( $imgFile, $imgFile, 0xFFFFFF, $wgLilypondBorderX, $wgLilypondBorderY );
        };
 
	return $pre . $link . $post;
}
 
function trimImage( $source, $dest, $bgColour )
{
  $srcImage = imagecreatefrompng( $source );
  $width = imagesx( $srcImage );
  $height = imagesy( $srcImage );
 
  $xmin = 0;
  $found = false;
  for( $x = 0; $x < $width && !$found; $x++ ) {
    for( $y = 0; $y < $height && !$found; $y++ ) {
      $rgb = imagecolorat( $srcImage, $x, $y );
      if( $rgb != $bgColour ) {
        $xmin = $x;
        $found = true;
      }
    }
  }
 
  $xmax = $xmin;
  $found = false;
  for( $x = $width-1; $x > $xmin && !$found; $x-- ) {
    for( $y = 0; $y < $height && !$found; $y++ ) {
      $rgb = imagecolorat( $srcImage, $x, $y );
      if( $rgb != $bgColour ) {
        $xmax = $x;
        $found = true;
      }
    }
  }
 
  $ymin = 0;
  $found = false;
  for( $y = 0; $y < $height && !$found; $y++ ) {
    for( $x = 0; $x < $width && !$found; $x++ ) {
      $rgb = imagecolorat( $srcImage, $x, $y );
      if( $rgb != $bgColour ) {
        $ymin = $y;
        $found = true;
      }
    }
  }
 
  $ymax = $ymin;
  $found = false;
  for( $y = $height-1; $y > $ymin && !$found; $y-- ) {
    for( $x = 0; $x < $width && !$found; $x++ ) {
      $rgb = imagecolorat( $srcImage, $x, $y );
      if( $rgb != $bgColour ) {
        $ymax = $y;
        $found = true;
      }
    }
  }
 
  $newWidth  = $xmax - $xmin + 1;
  $newHeight = $ymax - $ymin + 1;
 
  $dstImage = imagecreatetruecolor( $newWidth, $newHeight );
  imagecopy( $dstImage, $srcImage, 0, 0, $xmin, $ymin, $newWidth, $newHeight );
  imagepng( $dstImage, $dest );
}
 
function frameImage( $source, $dest, $bgColour, $borderWidth, $borderHeight )
{
  $srcImage = imagecreatefrompng( $source );
  $width = imagesx( $srcImage );
  $height = imagesy( $srcImage );
  $dstImage = imagecreatetruecolor( $width + 2*$borderWidth, $height + 2*$borderHeight );
  $allocatedBgColour = imagecolorallocate( $dstImage, ($bgColour >> 16) & 0xFF, ($bgColour >> 8) & 0xFF, $bgColour & 0xFF);
  imagefill( $dstImage, 0, 0, $allocatedBgColour );
  imagecopy( $dstImage, $srcImage, $borderWidth, $borderHeight, 0, 0, $width, $height );
  imagepng( $dstImage, $dest );
}