/*
* @(#)SVGImage.java 1.15 04/04/13
*
* � Copyright 2004 Vodafone Group Vodafone House The Connection Newbury
* RG14 2FN England All rights reserved.
*/
/*
* Portions Copyright 2000-2009 Sun Microsystems, Inc. All Rights
* Reserved. Use is subject to license terms.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.vodafone.lcdui;
import javax.microedition.lcdui.Graphics;
import java.io.InputStream;
import java.io.IOException;
import javax.microedition.io.ConnectionNotFoundException;
import java.io.ByteArrayOutputStream;
import javax.microedition.io.Connector;
import javax.microedition.io.StreamConnection;
import javax.microedition.m2g.ScalableGraphics;
import javax.microedition.m2g.ScalableImage;
import com.sun.perseus.model.SVG;
import com.sun.perseus.j2d.Transform;
import com.sun.perseus.model.DocumentNode;
import org.w3c.dom.svg.SVGException;
/**
* The <code>SVGImage</code> class allows applications to manipulate and
* render SVG Tiny 1.1 images on MIDP.
* <br />
*
* <p>An SVG image is scalable, which means it can be scaled to
* the desired size. In addition, it is possible to zoom into
* the image (and see details bigger) or zoom out (and see more of the
* image). Similarly, it is possible to rotate the image content and
* pan (translate) the image. All these manipluations are done without
* pixelation effects, providing a high quality rendering at any resolution
* zoom factor or rotation angle.</p>
*
* <p>An SVG image uses the SVG concept of viewport as defined
* by the SVG specification. The viewport is defined in a
* coordinate space called the viewport space where one unit is equal
* to the size of one device pixel, the positive x-axis direction
* running from left to right and the positive y-axis direction
* running from top to bottom. The viewport size after initially creating
* an <code>SVGImage</code> is defined by the width and height
* attributes on the root <code><svg></code> element. If
* these values use percentage coordinates, a reference value
* of 100 is used. That means that if the root <code><svg></code>
* element has a width of '50%', then the computed viewport width will
* be 50. If the root <code><svg></code> element does not specify
* width/height attributes, default values of 100%/100% are used according
* to the SVG specification. Applying these to the reference value of 100
* yields a default width/height of 100. The viewport size can be
* determined using the <code>getViewportWidth</code> and
* <code>getViewportHeight</code> methods. The initial viewport size
* can be overridden through the <code>setViewportSize</code> method.</p>
*
* <p>The <code>SVGImage</code> will fit the SVG content into the viewport
* when the content is rendered during a call to the <code>drawImage</code>
* method.</p>
*
* <p>The content is fit by applying a transform from the SVG user space to the
* viewport space. This is called the user space to viewport space transform:
* <code>F</code>. <code>F</code> accounts for the image's
* <code>preserveAspectRatio</code> and <code>viewBox</code>
* attributes.</p>
*
* <p>In addition, a user transform can be applied for the purpose of
* zooming, panning and rotating the document. This transform is called
* U.</p>
*
* <p>The effect of applying the various transforms is that a point in
* the SVG user space (<code>Pt[us]</code>) maps to a point in viewport
* space (<code>Pt[vp]</code>) through the following transform: </p>
*
* <pre>Pt[vp] = U.F.Pt[us]</pre>
*
* <p>In the above equation, <code>U</code> and <code>F</code> are affine
* transforms representing 3 by 3 matrices and <code>Pt[x]</code> are
* points with the conventional <code>[x, y, 1]</code> notation. The
* "<code>.</code>" operator represents a multiplication between two
* matrices or a matrix and a point.</p>
*
* <p>An SVG Image can be drawn as follows:</p>
* <pre>
* String svgURI = "http://www.foo.com/svgMap.svg";
* SVGImage svgRenderable
* = SVGImage.createImage(svgURI);
* svgRenderable.setViewportSize(200, 300);
* svgImage.zoom(2);
*
* Graphics g = ...;
* svgRenderable.drawImage(g, 40, 60);
* </pre>
*
* <p>In SVG Tiny, URI references are found on the use, the anchor and the
* image elements. The following paragraphs describe the error handling for
* each of these elements in a <code>createImage</code> call.</p>
*
* <p>URI references on anchor elements are ignored in this API and never cause
* an IOException, even when invalid.</p>
*
* <p>URI references on use elements must be valid, non-circular, local
* references. Any other reference (e.g., non-local, circular or invalid)
* causes the document to be in error and an IOException is thrown.</p>
*
* <p>URI references on image elements should be valid external references.
* Valid or invalid local references on image elements are an error and an
* IOException is thrown. Invalid external references on an image element
* do not cause the document to be in error and nothing should be drawn
* for the image element which cannot be retrieved.</p>
*
* @see <a href="http://www.w3.org/TR/SVG11/">
* Scalable Vector Graphics (SVG) 1.1 Specification</a>
* @see <a href=" http://www.w3.org/TR/SVGMobile/">
* Mobile SVG Profiles: SVG Tiny and SVG Basic</a>
* @since VSCL2.0
*/
public class SVGImage {
/**
* Error message when an invalid URI is passed to the createImage method.
*/
public static final String ERROR_INVALID_URI = "Invalid URI";
/**
* Error message when a null URI is passed to the createImage method.
*/
public static final String ERROR_NULL_URI = "Null URI parameter";
/**
* Error message when the viewport to user space transform cannot be computed.
*/
public static final String ERROR_NON_INVERTIBLE_SVG_TRANSFORM = "The viewport to user space transform " +
"cannot be computed. Check that the viewBox is not zero width and/or height";
/**
* The corresponding JSR 226 ScalableImage
*/
protected ScalableImage scalableImage;
/**
* ScalableGraphics instance used to paint the associated ScalableImage.
*/
protected ScalableGraphics sg;
/**
* The root SVG element.
*/
protected SVG svg;
/**
* The current user transform.
*/
protected Transform userTransform = new Transform(null);
/**
* A work transform, used to avoid creating too many instances.
*/
protected Transform workTransform = new Transform(null);
/**
* The displayed SVG Document instance.
*/
protected DocumentNode doc;
/**
* Constructor is package protected and cannot be called by
* applications directly. Applications should use the
* <code>createImage</code> factory method to build new
* <code>SVGImage</code> instances.
*
* @param scalableImage the associated ScalableImage instance. Should
* not be null.
* @see #createImage
* @throws NullPointerException if the input scalableImage parameter is null.
*/
SVGImage(final ScalableImage scalableImage) {
if (scalableImage == null) {
throw new NullPointerException();
}
this.scalableImage = scalableImage;
this.doc = (DocumentNode) ((javax.microedition.m2g.SVGImage) scalableImage).getDocument();
this.svg = (SVG) doc.getDocumentElement();
this.sg = ScalableGraphics.createInstance();
}
/**
* Creates a new <code>SVGImage</code>. The image's initial viewport
* size is determined as described above. The initial user transform
* is set to identity which is the same state as after calling the
* <code>resetTransform</code> method.
*
* @param svgURI describes an SVG URI to be loaded
* by the appropriate protocol handler, as described in the
* <code>javax.microedition.io.Connector</code> class.
* @return an SVGImage from the URI
*
* @throws IOException if there is an IO error while reading
* the SVG content. An exception is also thrown if the
* SVG document is in error, as specified by Appendix F
* of the SVG 1.1 specification. Finally, an IOException
* is thrown if the SVG content contains non-local,
* circular, or invalid URI references on use elements
* or any local URI references on image elements (see
* the class comments).
* @throws IllegalArgumentException if the svgURI parameter is invalid.
*
* @see #createImage(java.io.InputStream)
*/
public static SVGImage createImage(String svgURI) throws IOException {
ScalableImage scalableImage = null;
try {
scalableImage = ScalableImage.createImage(svgURI, null);
} catch (NullPointerException npe) {
throw new IllegalArgumentException(ERROR_NULL_URI);
}
return new SVGImage(scalableImage);
}
/**
* Creates a new <code>SVGImage</code>. The image's initial viewport
* size is determined as described above. The initial user transform
* is set to identity which is the same state as after calling the
* <code>resetTransform</code> method.
*
* @param is the input stream from which the SVG content
* will be read.
* @return an SVGImage from the stream
*
* @throws IOException if there is an IO error while reading
* the SVG content. An exception is also thrown if the
* SVG document is in error, as specified by Appendix F
* of the SVG 1.1 specification. Finally, an IOException
* is thrown if the SVG content contains non-local,
* circular, or invalid URI references on use elements
* or any local URI references on image elements (see
* the class comments).
*
* @see #createImage(java.lang.String)
*/
public static SVGImage createImage(InputStream is) throws IOException {
ScalableImage scalableImage = ScalableImage.createImage(is, null);
return new SVGImage(scalableImage);
}
/**
* Sets the SVG image's viewport width and height.
*
* @param width the new viewport width
* @param height the new viewport height
*
* @throws IllegalArgumentException if width is less than
* 0 or height is less than 0.
*
* @see #getViewportWidth
* @see #getViewportHeight
*/
public void setViewportSize(int width, int height)
throws IllegalArgumentException {
if (( width < 0 ) || (height < 0))
throw new IllegalArgumentException();
scalableImage.setViewportWidth(width);
scalableImage.setViewportHeight(height);
}
/**
* Returns the viewport's width.
*
* @return the current viewport width
* @see #setViewportSize
*/
public int getViewportWidth() {
return scalableImage.getViewportWidth();
}
/**
* Returns the viewport's height.
*
* @return the current viewport height
* @see #setViewportSize
*/
public int getViewportHeight() {
return scalableImage.getViewportHeight();
}
/**
* Renders the SVG image into the <code>Graphics</code>
* object. The viewport is the (0, 0, viewportWidth, viewportHeight)
* rectangle in viewport space. This rectangle is positioned on the
* canvas by the x and y parameters.
*
* <p>The viewportWidth and viewportHeight are lengths in the
* viewport coordinate space, which means they represent the
* number of device pixels for the width and height of the image.</p>
*
* <p>Note that the <code>Graphics</code>'s translation (set by
* the <code>translate</code> method) affects the location of
* the viewport as well. For example, consider the following code
* snippet:</p>
*
* <pre>
* // Initially, the graphics context origin is (0, 0)
* Graphics g = ...;
*
* // Change the graphics origin to (10, 30)
* g.translate(10, 30);
*
* // Draw an SVGImage
* SVGImage svgImage = ...;
* svgImage.drawImage(g, 20, 40);
* </pre>
*
* <p>The viewport will be positionned at <code>(30, 70)</code>
* because the <code>drawImage</code> call uses the <code>Graphics</code>
* translation and the viewport's origin.</p>
*
* <p>In mathematical terms, the following transform is used to
* transform viewport coordinates to graphics coordinates:</p>
* <pre>
* [1 0 tx+x]
* [0 1 ty+y]
* [0 0 1 ]
* </pre>
* <p>Where <code>tx</code> is the current <code>Graphics</code>
* translation along the x-axis, <code>ty</code> is the
* current <code>Graphics</code> translation along the y-axis,
* <code>x</code> is the origin of the viewport along the x-axis
* and <code>y</code> is the origin of the viewport along the
* y-axis. </p>
*
* <p>In addition, rendering is limited to the area defined by the
* current <code>Graphics</code>'s clip. The area which may be impacted
* by this call is the intersection of the viewport and the current
* clip.</p>
*
* @param g the <code>Graphics</code> where the SVG content
* is drawn.
* @param x the location, along the x-axis, where the SVG content
* will be drawn. This is the x location of the viewport
* on the canvas.
* @param y the location, along the y-axis, where the SVG content
* will be drawn. This is the y location of the viewport
* on the canvas.
*
* @throws NullPointerException if the <code>Graphics g</code> is null.
*
* @see javax.microedition.lcdui.Graphics
*/
public void drawImage(Graphics g, int x, int y) {
sg.bindTarget(g);
sg.render(x, y, scalableImage);
sg.releaseTarget();
}
/**
* Maps the input viewport coordinate to an SVG user space
* coordinate. Input coordinates may be outside the
* <code>[0, 0, viewportWidth, viewportHeight]</code>
* rectangle.
*
* <p>The transformation accounts for the SVG user space
* to viewport space transform due to the processing
* of preserveAspectRatio and viewBox on the root
* <code><svg></code> element as well as any user transform
* (see the <code>zoom</code>, <code>pan</code> and
* <code>rotate</code> methods). Using the <code>U</code>
* and <code>F</code> notation from the class description,
* the transform applied to the input coordinate is:
* <code>inverse(U.F)</code>, where the <code>inverse()</code>
* function is such that inverse(U.F).U.F is equal to the
* identity transform.</p>
*
* @param viewportCoordinate an array of two integer values,
* in the viewport coordinate space. The first array value
* is the x-axis coordinate and the second value is the
* y-axis coordinate.
* @param userSpaceCoordinate an array of two floating point
* values, in the SVG user space coordinate system. This
* is where the result is written.The first array value
* is the x-axis coordinate and the second value is the
* y-axis coordinate.
*
* @throws IllegalArgumentException if one or both of the
* input arguments are null or if they are of
* a length different than 2.
*
* @see #toViewportSpace
*/
public void toUserSpace(int[] viewportCoordinate,
float[] userSpaceCoordinate) {
if (( userSpaceCoordinate == null ) || ( viewportCoordinate == null )) {
throw new IllegalArgumentException();
}
if (( userSpaceCoordinate.length != 2 ) || ( viewportCoordinate.length != 2 )) {
throw new IllegalArgumentException();
}
// Get the current screenCTM
Transform txf = svg.getTransformState();
try {
txf = (Transform) txf.inverse();
} catch (SVGException se) {
throw new IllegalStateException(ERROR_NON_INVERTIBLE_SVG_TRANSFORM);
}
float[] pt = new float[2];
pt[0] = viewportCoordinate[0];
pt[1] = viewportCoordinate[1];
txf.transformPoint(pt, userSpaceCoordinate);
}
/**
* Maps the input SVG user space coordinate to a viewport coordinate.
*
* <p>The transformation applied includes the user space
* to viewport space transform due to the processing
* of preserveAspectRatio and viewBox on the root
* <code><svg></code> element as well as any user transform
* (see the <code>zoom</code>, <code>pan</code> and
* <code>rotate</code> methods).Using the <code>U</code>
* and <code>F</code> notation from the class description,
* the transform applied to the input coordinate is:
* <code>U.F</code></p>
*
* <p>Note that the resulting coordinate may be outside the
* <code>[0, 0, viewportWidth, viewportHeight]</code>
* rectangle.</p>
*
* @param userSpaceCoordinate an array of two floating point
* values, in the user space coordinate system.
* @param viewportCoordinate an array of two integer values,
* in the viewport coordinate space.
* This is where the result is written.
*
* @throws IllegalArgumentException if one or both of the
* input arguments are null or if they are of
* a size different than 2.
*
* @see #toUserSpace
*/
public void toViewportSpace(float[] userSpaceCoordinate,
int[] viewportCoordinate) {
if (( userSpaceCoordinate == null ) || ( viewportCoordinate == null )) {
throw new IllegalArgumentException();
}
if (( userSpaceCoordinate.length != 2 ) || ( viewportCoordinate.length != 2 )) {
throw new IllegalArgumentException();
}
// Get the current screenCTM
Transform txf = svg.getTransformState();
float[] pt = new float[2];
pt[0] = viewportCoordinate[0];
pt[1] = viewportCoordinate[1];
txf.transformPoint(userSpaceCoordinate, pt);
viewportCoordinate[0] = (int) pt[0];
viewportCoordinate[1] = (int) pt[1];
}
/**
* Preconcatenates a zoom transform to the current user
* transform.
* <p>As a result of invoking this method, the
* new user transform becomes:</p>
* <pre>
* U' = Z.U
* </pre>
* <p>Where U is the previous user transform and Z is the
* uniform scale transform:</p>
* <pre>
* [zoom 0 0]
* [ 0 zoom 0]
* [ 0 0 1]
* </pre>
* @param zoom the zoom factor to add to the user transform
*
* @throws IllegalArgumentException if the input zoom factor
* is not greater than zero.
*/
public void zoom(float zoom) {
if (zoom <=0) {
throw new IllegalArgumentException();
}
workTransform.setTransform(null);
workTransform.mScale(zoom);
workTransform.mMultiply(userTransform);
doc.setTransform(workTransform);
swapTransforms();
}
/**
* Preconcatenates a translation to the current user transform.
*
* <p>As a result of invoking this method, the
* new user transform becomes:</p>
* <pre>
* U' = T.U
* </pre>
* <p>Where U is the previous user transform and T is the
* following transform:</p>
* <pre>
* [1 0 panX]
* [0 1 panY]
* [0 0 1]
* </pre>
*
* @param panX the translation along the x-axis
* @param panY the translation along the y-axis
*/
public void pan(float panX, float panY) {
workTransform.setTransform(null);
workTransform.mTranslate(panX, panY);
workTransform.mMultiply(userTransform);
doc.setTransform(workTransform);
swapTransforms();
}
/**
* Preconcatenates a rotation about the given center to the
* current user transform.
*
* <p>As a result of invoking this method, the
* new user transform becomes:</p>
* <pre>
* U' = R.U
* </pre>
* <p>Where U is the previous user transform and R is the
* uniform scale transform:</p>
* <pre>
* [1 0 x][cos(theta) -sin(theta) 0][1 0 -x]
* [0 1 y][sin(theta) cos(theta) 0][0 1 -y]
* [0 0 1][ 0 0 1][0 0 1]
* </pre>
*
* <p>Postive angles go clockwise because of the orientation
* of the coordinate system.</p>
*
* @param theta the additional angle, in degrees
* @param x the rotation center along the x-axis
* @param y the rotation center along the y-axis
*/
public void rotate(float theta, float x, float y) {
workTransform.setTransform(null);
workTransform.mTranslate(x, y);
workTransform.mRotate(theta);
workTransform.mTranslate(-x, -y);
workTransform.mMultiply(userTransform);
doc.setTransform(workTransform);
swapTransforms();
}
/**
* Resets the user transform to identity.
*
* <p>As a result, the user transform becomes:</p>
* <pre>
* [1 0 0]
* [0 1 0]
* [0 0 1]
* </pre>
*/
public void resetUserTransform() {
workTransform.setTransform(null);
doc.setTransform(workTransform);
swapTransforms();
}
/**
* Implementation helpers. Swaps the workTransform and the
* userTransform values.
*/
private void swapTransforms() {
Transform tmp = workTransform;
workTransform = userTransform;
userTransform = tmp;
}
}