Canvas text spec implementation (bug 436904)
authorEric Butler <ebutler@mozilla.com>
Tue, 10 Jun 2008 16:16:59 -0700
changeset 15295 963ced95e2d1f10252fd3c54897d81b0883c9f05
parent 15294 5d6c15a287f7565085b8eeb213979b4065d2b3b8
child 15296 6c3e1f90c89bed9c880f977aab1496cfaf2ac113
push id1
push userroot
push dateTue, 26 Apr 2011 22:38:44 +0000
treeherdermozilla-beta@bfdb6e623a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
bugs436904
milestone1.9.1a1pre
Canvas text spec implementation (bug 436904)
content/canvas/src/nsCanvasRenderingContext2D.cpp
content/canvas/test/Makefile.in
content/canvas/test/test_text.font.html
content/canvas/test/test_text.measure.html
content/canvas/test/test_text.space.replace.html
content/canvas/test/test_text.textAlign.html
content/canvas/test/test_text.textBaseline.html
dom/public/idl/canvas/nsIDOMCanvasRenderingContext2D.idl
dom/public/nsDOMClassInfoID.h
dom/src/base/nsDOMClassInfo.cpp
layout/reftests/canvas/reftest.list
layout/reftests/canvas/text-big-fill.html
layout/reftests/canvas/text-big-stroke.html
layout/reftests/canvas/text-blank.html
layout/reftests/canvas/text-context-state-ref.html
layout/reftests/canvas/text-context-state-test.html
layout/reftests/canvas/text-font-inherit.html
layout/reftests/canvas/text-horzline-with-bottom.html
layout/reftests/canvas/text-horzline-with-top.html
layout/reftests/canvas/text-horzline.html
layout/reftests/canvas/text-ltr-alignment-ref.html
layout/reftests/canvas/text-ltr-alignment-test.html
layout/reftests/canvas/text-ltr-end.html
layout/reftests/canvas/text-ltr-left.html
layout/reftests/canvas/text-ltr-right.html
layout/reftests/canvas/text-ltr-start.html
layout/reftests/canvas/text-rtl-alignment-ref.html
layout/reftests/canvas/text-rtl-alignment-test.html
layout/reftests/canvas/text-rtl-end.html
layout/reftests/canvas/text-rtl-left.html
layout/reftests/canvas/text-rtl-right.html
layout/reftests/canvas/text-rtl-start.html
layout/reftests/canvas/text-space-replace-ref.html
layout/reftests/canvas/text-space-replace-test.html
layout/reftests/reftest.list
--- a/content/canvas/src/nsCanvasRenderingContext2D.cpp
+++ b/content/canvas/src/nsCanvasRenderingContext2D.cpp
@@ -270,16 +270,53 @@ NS_IMPL_RELEASE(nsCanvasPattern)
 NS_INTERFACE_MAP_BEGIN(nsCanvasPattern)
   NS_INTERFACE_MAP_ENTRY(nsCanvasPattern)
   NS_INTERFACE_MAP_ENTRY(nsIDOMCanvasPattern)
   NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(CanvasPattern)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
 
 /**
+ ** nsTextMetrics
+ **/
+#define NS_TEXTMETRICS_PRIVATE_IID \
+    { 0xc5b1c2f9, 0xcb4f, 0x4394, { 0xaf, 0xe0, 0xc6, 0x59, 0x33, 0x80, 0x8b, 0xf3 } }
+class nsTextMetrics : public nsIDOMTextMetrics
+{
+public:
+    nsTextMetrics(float w) : width(w) { }
+
+    virtual ~nsTextMetrics() { }
+
+    NS_DECLARE_STATIC_IID_ACCESSOR(NS_TEXTMETRICS_PRIVATE_IID)
+
+    NS_IMETHOD GetWidth(float* w) {
+        *w = width;
+        return NS_OK;
+    }
+
+    NS_DECL_ISUPPORTS
+
+private:
+    float width;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsTextMetrics, NS_TEXTMETRICS_PRIVATE_IID)
+
+NS_IMPL_ADDREF(nsTextMetrics)
+NS_IMPL_RELEASE(nsTextMetrics)
+
+NS_INTERFACE_MAP_BEGIN(nsTextMetrics)
+  NS_INTERFACE_MAP_ENTRY(nsTextMetrics)
+  NS_INTERFACE_MAP_ENTRY(nsIDOMTextMetrics)
+  NS_INTERFACE_MAP_ENTRY_CONTENT_CLASSINFO(TextMetrics)
+  NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/**
  ** nsCanvasRenderingContext2D
  **/
 class nsCanvasRenderingContext2D :
     public nsIDOMCanvasRenderingContext2D,
     public nsICanvasRenderingContextInternal
 {
 public:
     nsCanvasRenderingContext2D();
@@ -348,31 +385,69 @@ protected:
     // yay cairo
     nsRefPtr<gfxContext> mThebesContext;
     nsRefPtr<gfxASurface> mThebesSurface;
 
     PRUint32 mSaveCount;
     cairo_t *mCairo;
     cairo_surface_t *mSurface;
 
-    nsString mTextStyle;
-    nsRefPtr<gfxFontGroup> mFontGroup;
+    // text
+    enum TextAlign {
+        TEXT_ALIGN_START,
+        TEXT_ALIGN_END,
+        TEXT_ALIGN_LEFT,
+        TEXT_ALIGN_RIGHT,
+        TEXT_ALIGN_CENTER
+    };
+
+    enum TextBaseline {
+        TEXT_BASELINE_TOP,
+        TEXT_BASELINE_HANGING,
+        TEXT_BASELINE_MIDDLE,
+        TEXT_BASELINE_ALPHABETIC,
+        TEXT_BASELINE_IDEOGRAPHIC,
+        TEXT_BASELINE_BOTTOM
+    };
+
     gfxFontGroup *GetCurrentFontStyle();
+
+    enum TextDrawOperation {
+        TEXT_DRAW_OPERATION_FILL,
+        TEXT_DRAW_OPERATION_STROKE
+    };
+
+    /*
+     * Implementation of the fillText and strokeText functions with the
+     * operation abstracted to a flag. Follows the HTML 5 spec for rendering
+     * text. Will query for the fourth, optional argument.
+     */
+    nsresult drawText(const nsAString& text,
+                      float x,
+                      float y,
+                      float maxWidth,
+                      TextDrawOperation op);
  
     // style handling
     PRInt32 mLastStyle;
     PRPackedBool mDirtyStyle[STYLE_MAX];
 
     // state stack handling
     class ContextState {
     public:
-        ContextState() : globalAlpha(1.0) { }
+        ContextState() : globalAlpha(1.0),
+                         textAlign(TEXT_ALIGN_START),
+                         textBaseline(TEXT_BASELINE_ALPHABETIC) { }
 
         ContextState(const ContextState& other)
-            : globalAlpha(other.globalAlpha)
+            : globalAlpha(other.globalAlpha),
+              font(other.font),
+              fontGroup(other.fontGroup),
+              textAlign(other.textAlign),
+              textBaseline(other.textBaseline)
         {
             for (int i = 0; i < STYLE_MAX; i++) {
                 colorStyles[i] = other.colorStyles[i];
                 gradientStyles[i] = other.gradientStyles[i];
                 patternStyles[i] = other.patternStyles[i];
             }
         }
 
@@ -388,16 +463,22 @@ protected:
         }
 
         inline void SetGradientStyle(int whichStyle, nsCanvasGradient* grad) {
             gradientStyles[whichStyle] = grad;
             patternStyles[whichStyle] = nsnull;
         }
 
         float globalAlpha;
+
+        nsString font;
+        nsRefPtr<gfxFontGroup> fontGroup;
+        TextAlign textAlign;
+        TextBaseline textBaseline;
+
         nscolor colorStyles[STYLE_MAX];
         nsCOMPtr<nsCanvasGradient> gradientStyles[STYLE_MAX];
         nsCOMPtr<nsCanvasPattern> patternStyles[STYLE_MAX];
     };
 
     nsTArray<ContextState> mStyleStack;
 
     inline ContextState& CurrentState() {
@@ -1483,20 +1564,21 @@ nsCanvasRenderingContext2D::Rect(float x
 
     cairo_rectangle (mCairo, x, y, w, h);
     return NS_OK;
 }
 
 //
 // text
 //
+
 NS_IMETHODIMP
-nsCanvasRenderingContext2D::SetMozTextStyle(const nsAString& textStyle)
+nsCanvasRenderingContext2D::SetFont(const nsAString& font)
 {
-    if(mTextStyle.Equals(textStyle)) return NS_OK;
+    if(CurrentState().font.Equals(font)) return NS_OK;
 
     nsCOMPtr<nsINode> elem = do_QueryInterface(mCanvasElement);
     if (!elem) {
         NS_WARNING("Canvas element must be an nsINode and non-null");
         return NS_ERROR_FAILURE;
     }
 
     nsIPrincipal* elemPrincipal = elem->NodePrincipal();
@@ -1524,65 +1606,422 @@ nsCanvasRenderingContext2D::SetMozTextSt
     mCSSParser->ParseStyleAttribute(
             EmptyString(),
             docURL,
             baseURL,
             elemPrincipal,
             getter_AddRefs(rule));
 
     mCSSParser->ParseProperty(eCSSProperty_font,
-                              textStyle,
+                              font,
                               docURL,
                               baseURL,
                               elemPrincipal,
                               rule->GetDeclaration(),
                               &changed);
 
     rules.AppendObject(rule);
 
     nsStyleSet *styleSet = presShell->StyleSet();
 
-    nsRefPtr<nsStyleContext> sc = styleSet->ResolveStyleForRules(nsnull,rules);
+    // get the frame's style context to use as the parent
+    if (!mCanvasElement) {
+        return NS_ERROR_FAILURE;
+    }
+    nsIFrame* frame;
+    if (mCanvasElement->GetPrimaryCanvasFrame(&frame)!=NS_OK) {
+        return NS_ERROR_FAILURE;
+    }
+
+    nsRefPtr<nsStyleContext> sc = styleSet->ResolveStyleForRules(frame->GetStyleContext(), rules);
     const nsStyleFont *fontStyle = sc->GetStyleFont();
 
     NS_ASSERTION(fontStyle, "Could not obtain font style");
 
     PRUint32 aupdp = presShell->GetPresContext()->AppUnitsPerDevPixel();
 
     gfxFontStyle style(fontStyle->mFont.style,
                        fontStyle->mFont.weight,
                        NSAppUnitsToFloatPixels(fontStyle->mFont.size,aupdp),
                        langGroup,
                        fontStyle->mFont.sizeAdjust,
                        fontStyle->mFont.systemFont,
                        fontStyle->mFont.familyNameQuirks);
 
-    mFontGroup = gfxPlatform::GetPlatform()->CreateFontGroup(fontStyle->mFont.name, &style);
-    NS_ASSERTION(mFontGroup, "Could not get font group");
-    mTextStyle = textStyle;
+    CurrentState().fontGroup = gfxPlatform::GetPlatform()->CreateFontGroup(fontStyle->mFont.name, &style);
+    NS_ASSERTION(CurrentState().fontGroup, "Could not get font group");
+    CurrentState().font = font;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::GetFont(nsAString& font)
+{
+    /* will initilize the value if not set, else does nothing */
+    GetCurrentFontStyle();
+
+    font = CurrentState().font;
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::SetTextAlign(const nsAString& ta)
+{
+    if (ta.EqualsLiteral("start"))
+        CurrentState().textAlign = TEXT_ALIGN_START;
+    else if (ta.EqualsLiteral("end"))
+        CurrentState().textAlign = TEXT_ALIGN_END;
+    else if (ta.EqualsLiteral("left"))
+        CurrentState().textAlign = TEXT_ALIGN_LEFT;
+    else if (ta.EqualsLiteral("right"))
+        CurrentState().textAlign = TEXT_ALIGN_RIGHT;
+    else if (ta.EqualsLiteral("center"))
+        CurrentState().textAlign = TEXT_ALIGN_CENTER;
+    // spec says to not throw error for invalid arg, but do it anyway
+    else
+        return NS_ERROR_INVALID_ARG;
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::GetTextAlign(nsAString& ta)
+{
+    switch (CurrentState().textAlign)
+    {
+    case TEXT_ALIGN_START:
+        ta.AssignLiteral("start");
+        break;
+    case TEXT_ALIGN_END:
+        ta.AssignLiteral("end");
+        break;
+    case TEXT_ALIGN_LEFT:
+        ta.AssignLiteral("left");
+        break;
+    case TEXT_ALIGN_RIGHT:
+        ta.AssignLiteral("right");
+        break;
+    case TEXT_ALIGN_CENTER:
+        ta.AssignLiteral("center");
+        break;
+    default:
+        NS_ASSERTION(0, "textAlign holds invalid value");
+        return NS_ERROR_FAILURE;
+    }
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::SetTextBaseline(const nsAString& tb)
+{
+    if (tb.EqualsLiteral("top"))
+        CurrentState().textBaseline = TEXT_BASELINE_TOP;
+    else if (tb.EqualsLiteral("hanging"))
+        CurrentState().textBaseline = TEXT_BASELINE_HANGING;
+    else if (tb.EqualsLiteral("middle"))
+        CurrentState().textBaseline = TEXT_BASELINE_MIDDLE;
+    else if (tb.EqualsLiteral("alphabetic"))
+        CurrentState().textBaseline = TEXT_BASELINE_ALPHABETIC;
+    else if (tb.EqualsLiteral("ideographic"))
+        CurrentState().textBaseline = TEXT_BASELINE_IDEOGRAPHIC;
+    else if (tb.EqualsLiteral("bottom"))
+        CurrentState().textBaseline = TEXT_BASELINE_BOTTOM;
+    // spec says to not throw error for invalid arg, but do it anyway
+    else
+        return NS_ERROR_INVALID_ARG;
+    
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::GetTextBaseline(nsAString& tb)
+{
+    switch (CurrentState().textBaseline)
+    {
+    case TEXT_BASELINE_TOP:
+        tb.AssignLiteral("top");
+        break;
+    case TEXT_BASELINE_HANGING:
+        tb.AssignLiteral("hanging");
+        break;
+    case TEXT_BASELINE_MIDDLE:
+        tb.AssignLiteral("middle");
+        break;
+    case TEXT_BASELINE_ALPHABETIC:
+        tb.AssignLiteral("alphabetic");
+        break;
+    case TEXT_BASELINE_IDEOGRAPHIC:
+        tb.AssignLiteral("ideographic");
+        break;
+    case TEXT_BASELINE_BOTTOM:
+        tb.AssignLiteral("bottom");
+        break;
+    default:
+        NS_ASSERTION(0, "textBaseline holds invalid value");
+        return NS_ERROR_FAILURE;
+    }
+
     return NS_OK;
 }
 
+/*
+ * Helper function that replaces the whitespace characters in a string
+ * with U+0020 SPACE. The whitespace characters are defined as U+0020 SPACE,
+ * U+0009 CHARACTER TABULATION (tab), U+000A LINE FEED (LF), U+000B LINE
+ * TABULATION, U+000C FORM FEED (FF), and U+000D CARRIAGE RETURN (CR).
+ * @param str The string whose whitespace characters to replace.
+ */
+static inline void TextReplaceWhitespaceCharacters(nsAutoString& str)
+{
+    str.ReplaceChar("\x09\x0A\x0B\x0C\x0D", PRUnichar(' '));
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::FillText(const nsAString& text, float x, float y, float maxWidth)
+{
+    return drawText(text, x, y, maxWidth, TEXT_DRAW_OPERATION_FILL);
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::StrokeText(const nsAString& text, float x, float y, float maxWidth)
+{
+    return drawText(text, x, y, maxWidth, TEXT_DRAW_OPERATION_STROKE);
+}
+
+nsresult
+nsCanvasRenderingContext2D::drawText(const nsAString& rawText,
+                                     float x,
+                                     float y,
+                                     float maxWidth,
+                                     TextDrawOperation op)
+{
+    if (!FloatValidate(x, y, maxWidth))
+        return NS_ERROR_DOM_SYNTAX_ERR;
+
+    // get js context to parse extra arg
+    nsresult rv;
+
+    // spec isn't clear on what should happen if maxWidth <= 0, so
+    // treat it as an invalid argument
+    // technically, 0 should be an invalid value as well, but 0 is the default
+    // arg, and there is no way to tell if the default was used
+    if (maxWidth < 0)
+        return NS_ERROR_INVALID_ARG;
+
+    gfxFontGroup* fontgrp = GetCurrentFontStyle();
+    NS_ASSERTION(fontgrp, "font group is null");
+
+    // replace all the whitespace characters with U+0020 SPACE
+    nsAutoString textToDraw(rawText);
+    TextReplaceWhitespaceCharacters(textToDraw);
+
+    const PRUnichar* textData;
+    textToDraw.GetData(&textData);
+
+    // get direction property from the frame
+    nsIFrame* frame;
+    rv = mCanvasElement->GetPrimaryCanvasFrame(&frame);
+    if (NS_FAILED(rv))
+        return rv;
+  
+    PRBool isRTL = frame->GetStyleVisibility()->mDirection ==
+        NS_STYLE_DIRECTION_RTL;
+
+    PRUint32 textrunflags = isRTL ? gfxTextRunFactory::TEXT_IS_RTL : 0;
+
+    // app units conversion factor
+    PRUint32 aupdp;
+    GetAppUnitsValues(&aupdp, NULL);
+
+    gfxTextRunCache::AutoTextRun textRun;
+    textRun = gfxTextRunCache::MakeTextRun(textData,
+                                           textToDraw.Length(),
+                                           fontgrp,
+                                           mThebesContext,
+                                           aupdp,
+                                           textrunflags);
+
+    if (!textRun.get())
+        return NS_ERROR_FAILURE;
+
+    gfxPoint pt(x, y);
+    
+    // get the text width
+    PRBool tightBoundingBox = PR_FALSE;
+    gfxTextRun::Metrics textRunMetrics = textRun->MeasureText(/* offset = */ 0,
+                                                       textToDraw.Length(),
+                                                       tightBoundingBox,
+                                                       mThebesContext,
+                                                       nsnull);
+    gfxFloat textWidth = textRunMetrics.mAdvanceWidth/gfxFloat(aupdp);
+
+
+    // offset pt x based on text align
+    gfxFloat anchorX;
+
+    if (CurrentState().textAlign == TEXT_ALIGN_CENTER)
+        anchorX = .5;
+    else if (CurrentState().textAlign == TEXT_ALIGN_LEFT ||
+             (!isRTL && CurrentState().textAlign == TEXT_ALIGN_START) ||
+             (isRTL && CurrentState().textAlign == TEXT_ALIGN_END))
+        anchorX = 0;
+    else
+        anchorX = 1;
+
+    if (isRTL)
+        pt.x += (1 - anchorX) * textWidth;
+    else
+        pt.x -= anchorX * textWidth;
+
+    // offset pt y based on text baseline
+    NS_ASSERTION(fontgrp->FontListLength()>0, "font group contains no fonts");
+    const gfxFont::Metrics& fontMetrics = fontgrp->GetFontAt(0)->GetMetrics();
+
+    gfxFloat anchorY;
+
+    switch (CurrentState().textBaseline)
+    {
+    case TEXT_BASELINE_TOP:
+        anchorY = fontMetrics.emAscent;
+        break;
+    case TEXT_BASELINE_HANGING:
+        anchorY = 0; // currently unavailable
+        break;
+    case TEXT_BASELINE_MIDDLE:
+        anchorY = (fontMetrics.emAscent-fontMetrics.emDescent)*.5f;
+        break;
+    case TEXT_BASELINE_ALPHABETIC:
+        anchorY = 0;
+        break;
+    case TEXT_BASELINE_IDEOGRAPHIC:
+        anchorY = 0; // currently unvailable
+        break;
+    case TEXT_BASELINE_BOTTOM:
+        anchorY = -fontMetrics.emDescent;
+        break;
+    default:
+        NS_ASSERTION(0, "mTextBaseline holds invalid value");
+        return NS_ERROR_FAILURE;
+    }
+
+    pt.y += anchorY;
+
+    // if text is over maxWidth, then scale the text horizonally such that its
+    // width is precisely maxWidth
+    if (maxWidth>0 && textWidth > maxWidth) {
+        cairo_save(mCairo);
+        // translate the anchor point to 0, then scale and translate back
+        cairo_translate(mCairo, x, 0);
+        cairo_scale(mCairo, (float)(maxWidth/textWidth), 1);
+        cairo_translate(mCairo, -x, 0);
+    }
+
+    pt.x *= aupdp;
+    pt.y *= aupdp;
+
+    // stroke or fill depending on operation
+    if (op == TEXT_DRAW_OPERATION_STROKE) {
+        cairo_save(mCairo);
+        cairo_new_path(mCairo);
+        textRun->DrawToPath(mThebesContext,
+                            pt,
+                            /* offset = */ 0,
+                            textToDraw.Length(),
+                            nsnull,
+                            nsnull);
+        Stroke();
+        cairo_restore(mCairo);
+    } else {
+        NS_ASSERTION(op == TEXT_DRAW_OPERATION_FILL,
+            "operation should be FILL or STROKE");
+
+        ApplyStyle(STYLE_FILL);
+        textRun->Draw(mThebesContext,
+                      pt,
+                      /* offset = */ 0,
+                      textToDraw.Length(),
+                      nsnull,
+                      nsnull,
+                      nsnull);
+    }
+
+    // have to restore the context if was modified above
+    if (maxWidth>0 && textWidth > maxWidth)
+        cairo_restore(mCairo);
+
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::MeasureText(const nsAString& rawText,
+                                        nsIDOMTextMetrics** _retval)
+{
+    // replace all the whitespace characters with U+0020 SPACE
+    nsAutoString textToMeasure(rawText);
+    TextReplaceWhitespaceCharacters(textToMeasure);
+
+    const PRUnichar* textdata;
+    textToMeasure.GetData(&textdata);
+
+    PRUint32 textrunflags = 0;
+    PRUint32 aupdp;
+    GetAppUnitsValues(&aupdp, nsnull);
+
+    gfxTextRunCache::AutoTextRun textRun;
+    textRun = gfxTextRunCache::MakeTextRun(textdata,
+                                           textToMeasure.Length(),
+                                           GetCurrentFontStyle(),
+                                           mThebesContext,
+                                           aupdp,
+                                           textrunflags);
+
+    if(!textRun.get())
+        return NS_ERROR_FAILURE;
+
+    PRBool tightBoundingBox = PR_FALSE;
+    gfxTextRun::Metrics metrics = textRun->MeasureText(/* offset = */ 0, textToMeasure.Length(),
+                                                       tightBoundingBox, mThebesContext,
+                                                       nsnull);
+
+    float textWidth = float(metrics.mAdvanceWidth/gfxFloat(aupdp));
+
+    nsTextMetrics *textMetrics = new nsTextMetrics(textWidth);
+    if (!textMetrics)
+        return NS_ERROR_OUT_OF_MEMORY;
+
+    NS_ADDREF(*_retval = textMetrics);
+    return NS_OK;
+}
+
+NS_IMETHODIMP
+nsCanvasRenderingContext2D::SetMozTextStyle(const nsAString& textStyle)
+{
+    // font and mozTextStyle are the same value
+    return SetFont(textStyle);
+}
+
 NS_IMETHODIMP
 nsCanvasRenderingContext2D::GetMozTextStyle(nsAString& textStyle)
 {
-    textStyle = mTextStyle;
-    return NS_OK;
+    // font and mozTextStyle are the same value
+    return GetFont(textStyle);
 }
 
 gfxFontGroup *nsCanvasRenderingContext2D::GetCurrentFontStyle()
 {
-    if(!mFontGroup)
-    {
-        nsString style;
-        style.AssignLiteral("12pt sans-serif");
+    // use lazy initilization for the font group since it's rather expensive
+    if(!CurrentState().fontGroup) {
+        nsAutoString style;
+        style.AssignLiteral("10px sans-serif");
         nsresult res = SetMozTextStyle(style);
         NS_ASSERTION(res == NS_OK, "Default canvas font is invalid");
     }
-    return mFontGroup;
+
+    return CurrentState().fontGroup;
 }
 
 NS_IMETHODIMP
 nsCanvasRenderingContext2D::MozDrawText(const nsAString& textToDraw)
 {
     const PRUnichar* textdata;
     textToDraw.GetData(&textdata);
 
@@ -1615,40 +2054,22 @@ nsCanvasRenderingContext2D::MozDrawText(
                   nsnull,
                   nsnull);
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsCanvasRenderingContext2D::MozMeasureText(const nsAString& textToMeasure, float *retVal)
 {
-    const PRUnichar* textdata;
-    textToMeasure.GetData(&textdata);
-
-    PRUint32 textrunflags = 0;
-    PRUint32 aupdp, aupcp;
-    GetAppUnitsValues(&aupdp, &aupcp);
-
-    gfxTextRunCache::AutoTextRun textRun;
-    textRun = gfxTextRunCache::MakeTextRun(textdata,
-                                           textToMeasure.Length(),
-                                           GetCurrentFontStyle(),
-                                           mThebesContext,
-                                           aupdp,
-                                           textrunflags);
-
-    if(!textRun.get())
-        return NS_ERROR_FAILURE;
-
-    PRBool tightBoundingBox = PR_FALSE;
-    gfxTextRun::Metrics metrics = textRun->MeasureText(/* offset = */ 0, textToMeasure.Length(),
-                                                       tightBoundingBox, mThebesContext,
-                                                       nsnull);
-    *retVal = float(metrics.mAdvanceWidth/gfxFloat(aupcp));
-    return NS_OK;
+    nsCOMPtr<nsIDOMTextMetrics> metrics;
+    nsresult rv;
+    rv = MeasureText(textToMeasure, getter_AddRefs(metrics));
+    if (NS_FAILED(rv))
+        return rv;
+    return metrics->GetWidth(retVal);
 }
 
 NS_IMETHODIMP
 nsCanvasRenderingContext2D::MozPathText(const nsAString& textToPath)
 {
     const PRUnichar* textdata;
     textToPath.GetData(&textdata);
 
@@ -1712,16 +2133,20 @@ nsCanvasRenderingContext2D::MozTextAlong
         PathChar() : draw(PR_FALSE), angle(0.0), pos(0.0,0.0) {}
     };
 
     gfxFloat length = path->GetLength();
     PRUint32 strLength = textToDraw.Length();
 
     PathChar *cp = new PathChar[strLength];
 
+    if (!cp) {
+        return NS_ERROR_OUT_OF_MEMORY;
+    }
+
     gfxPoint position(0.0,0.0);
     gfxFloat x = position.x;
     for (PRUint32 i = 0; i < strLength; i++)
     {
         gfxFloat halfAdvance = textRun->GetAdvanceWidth(i, 1, nsnull) / (2.0 * aupdp);
 
         // Check for end of path
         if(x + halfAdvance > length)
--- a/content/canvas/test/Makefile.in
+++ b/content/canvas/test/Makefile.in
@@ -498,16 +498,21 @@ include $(topsrcdir)/config/rules.mk
 	image_green-redirect^headers^ \
 	image_ggrr-256x256.png \
 	image_yellow75.png \
 	image_broken.png \
 	image_rgrg-256x256.png \
 	image_red.png \
 	image_transparent.png \
 	image_green.png \
+	test_text.font.html \
+	test_text.textAlign.html \
+	test_text.textBaseline.html \
+	test_text.measure.html \
+	test_text.space.replace.html \
 	$(NULL)
 
 # This one test crashes Mac for now.  Bug 407104
 ifneq ($(MOZ_WIDGET_TOOLKIT),cocoa)
 _TEST_FILES_E += \
 	test_2d.gradient.empty.html \
 	$(NULL)
 endif
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_text.font.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<title>Canvas test: text.font</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script>
+var _deferred = false;
+
+SimpleTest.waitForExplicitFinish();
+MochiKit.DOM.addLoadEvent(function () {
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+is(ctx.font, '10px sans-serif', "default font is not '10px sans-serif'");
+
+ctx.save();
+ctx.font = '20pt serif';
+is(ctx.font, '20pt serif', 'font getter returns incorrect value');
+
+ctx.restore();
+is(ctx.font, '10px sans-serif', 'font not being stored in the context state');
+
+if (!_deferred) SimpleTest.finish();
+});
+</script>
+
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_text.measure.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<title>Canvas test: text.measure</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script>
+var _deferred = false;
+
+SimpleTest.waitForExplicitFinish();
+MochiKit.DOM.addLoadEvent(function () {
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+ctx.font = "10px sans-serif";
+ctx.textAlign = "left";
+ctx.textBaseline = "top";
+
+var str = 'Test String';
+var wid = ctx.measureText(str).width;
+
+ok(wid > 0, "measureText returns nonpositive value for non-empty string");
+
+ctx.font = "20px sans-serif";
+isnot(wid, ctx.measureText(str).width, "measureText does not change with a different font size");
+
+ctx.font = "10px sans-serif";
+ctx.textAlign = "center";
+ctx.textBaseline = "alphabetic";
+
+is(wid, ctx.measureText(str).width, "measureText changes when alignement/baseline is changed");
+
+
+if (!_deferred) SimpleTest.finish();
+});
+</script>
+
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_text.space.replace.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<title>Canvas test: text.space.replace</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script>
+var _deferred = false;
+
+SimpleTest.waitForExplicitFinish();
+MochiKit.DOM.addLoadEvent(function () {
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+var swid = ctx.measureText(' ').width;
+ctx.font = "10px sans-serif";
+
+isnot(swid, 0.0, "measureText reutuns zero for a non-empty string");
+is(swid, ctx.measureText('\x09').width, "measureText does not replace whitespace char with a space");
+is(swid, ctx.measureText('\x0A').width, "measureText does not replace whitespace char with a space");
+is(swid, ctx.measureText('\x0B').width, "measureText does not replace whitespace char with a space");
+is(swid, ctx.measureText('\x0C').width, "measureText does not replace whitespace char with a space");
+is(swid, ctx.measureText('\x0D').width, "measureText does not replace whitespace char with a space");
+
+if (!_deferred) SimpleTest.finish();
+});
+</script>
+
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_text.textAlign.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<title>Canvas test: text.textAlign</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script>
+var _deferred = false;
+
+SimpleTest.waitForExplicitFinish();
+MochiKit.DOM.addLoadEvent(function () {
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+is(ctx.textAlign, 'start', "default textAlign is not 'start'");
+
+ctx.save();
+ctx.textAlign = 'end';
+is(ctx.textAlign, 'end', 'textAlign getter returns incorrect value');
+
+ctx.save();
+ctx.textAlign = 'left';
+is(ctx.textAlign, 'left', 'textAlign getter returns incorrect value');
+
+ctx.save();
+ctx.textAlign = 'center';
+is(ctx.textAlign, 'center', 'textAlign getter returns incorrect value');
+
+ctx.save();
+ctx.textAlign = 'right';
+is(ctx.textAlign, 'right', 'textAlign getter returns incorrect value');
+
+ctx.save();
+ctx.textAlign = 'start';
+is(ctx.textAlign, 'start', 'textAlign getter returns incorrect value');
+
+ctx.restore();
+is(ctx.textAlign, 'right', 'textAlign not being stored in the context state');
+
+ctx.restore();
+is(ctx.textAlign, 'center', 'textAlign not being stored in the context state');
+
+ctx.restore();
+is(ctx.textAlign, 'left', 'textAlign not being stored in the context state');
+
+ctx.restore();
+is(ctx.textAlign, 'end', 'textAlign not being stored in the context state');
+
+ctx.restore();
+is(ctx.textAlign, 'start', 'textAlign not being stored in the context state');
+
+if (!_deferred) SimpleTest.finish();
+});
+</script>
+
new file mode 100644
--- /dev/null
+++ b/content/canvas/test/test_text.textBaseline.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<title>Canvas test: text.textBaseline</title>
+<script src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+<body>
+<canvas id="c" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas>
+<script>
+var _deferred = false;
+
+SimpleTest.waitForExplicitFinish();
+MochiKit.DOM.addLoadEvent(function () {
+
+var canvas = document.getElementById('c');
+var ctx = canvas.getContext('2d');
+
+is(ctx.textBaseline, 'alphabetic', "default textBaseline is not 'alphabetic'");
+
+ctx.save();
+ctx.textBaseline = 'ideographic';
+is(ctx.textBaseline, 'ideographic', 'textBaseline getter returns incorrect value');
+
+ctx.save();
+ctx.textBaseline = 'top';
+is(ctx.textBaseline, 'top', 'textBaseline getter returns incorrect value');
+
+ctx.save();
+ctx.textBaseline = 'middle';
+is(ctx.textBaseline, 'middle', 'textBaseline getter returns incorrect value');
+
+ctx.save();
+ctx.textBaseline = 'bottom';
+is(ctx.textBaseline, 'bottom', 'textBaseline getter returns incorrect value');
+
+ctx.save();
+ctx.textBaseline = 'hanging';
+is(ctx.textBaseline, 'hanging', 'textBaseline getter returns incorrect value');
+
+ctx.save();
+ctx.textBaseline = 'alphabetic';
+is(ctx.textBaseline, 'alphabetic', 'textBaseline getter returns incorrect value');
+
+ctx.restore();
+is(ctx.textBaseline, 'hanging', 'textBaseline not being stored in the context state');
+
+ctx.restore();
+is(ctx.textBaseline, 'bottom', 'textBaseline not being stored in the context state');
+
+ctx.restore();
+is(ctx.textBaseline, 'middle', 'textBaseline not being stored in the context state');
+
+ctx.restore();
+is(ctx.textBaseline, 'top', 'textBaseline not being stored in the context state');
+
+ctx.restore();
+is(ctx.textBaseline, 'ideographic', 'textBaseline not being stored in the context state');
+
+ctx.restore();
+is(ctx.textBaseline, 'alphabetic', 'textBaseline not being stored in the context state');
+
+if (!_deferred) SimpleTest.finish();
+});
+</script>
+
--- a/dom/public/idl/canvas/nsIDOMCanvasRenderingContext2D.idl
+++ b/dom/public/idl/canvas/nsIDOMCanvasRenderingContext2D.idl
@@ -49,16 +49,22 @@ interface nsIDOMCanvasGradient : nsISupp
   void addColorStop(in float offset, in DOMString color);
 };
 
 [scriptable, uuid(21dea65c-5c08-4eb1-ac82-81fe95be77b8)]
 interface nsIDOMCanvasPattern : nsISupports
 {
 };
 
+[scriptable, uuid(2d01715c-ec7d-424a-ab85-e0fd70c8665c)]
+interface nsIDOMTextMetrics : nsISupports
+{
+  readonly attribute float width;
+};
+
 [scriptable, uuid(87d2b1c4-b64e-41f6-98f7-7ac4244ce9bf)]
 interface nsIDOMCanvasRenderingContext2D : nsISupports
 {
   // back-reference to the canvas element for which
   // this context was created
   readonly attribute nsIDOMHTMLCanvasElement canvas;
 
   // state
@@ -112,16 +118,26 @@ interface nsIDOMCanvasRenderingContext2D
   void arc(in float x, in float y, in float r, in float startAngle, in float endAngle, in boolean clockwise);
   void rect(in float x, in float y, in float w, in float h);
 
   void fill();
   void stroke();
   void clip();
 
   // text api
+  attribute DOMString font; /* default "10px sans-serif" */
+  attribute DOMString textAlign; /* "start" (default), "end", "left", "right",
+                                 "center" */
+  attribute DOMString textBaseline; /* "alphabetic" (default), "top", "hanging",
+                                    "middle", "ideographic", "bottom" */
+
+  void fillText(in DOMString text, in float x, in float y, [optional] in float maxWidth);
+  void strokeText(in DOMString text, in float x, in float y, [optional] in float maxWidth);
+  nsIDOMTextMetrics measureText(in DOMString text);
+
   attribute DOMString mozTextStyle; 
   void mozDrawText(in DOMString textToDraw);
   float mozMeasureText(in DOMString textToMeasure);
   void mozPathText(in DOMString textToPath);
   void mozTextAlongPath(in DOMString textToDraw, in boolean stroke);
 
   // image api
   void drawImage();
--- a/dom/public/nsDOMClassInfoID.h
+++ b/dom/public/nsDOMClassInfoID.h
@@ -348,16 +348,17 @@ enum nsDOMClassInfoID {
 #endif // MOZ_SVG
 
   // Canvas
   eDOMClassInfo_HTMLCanvasElement_id,
 #ifdef MOZ_ENABLE_CANVAS
   eDOMClassInfo_CanvasRenderingContext2D_id,
   eDOMClassInfo_CanvasGradient_id,
   eDOMClassInfo_CanvasPattern_id,
+  eDOMClassInfo_TextMetrics_id,
 #endif
 
   // SmartCard Events
   eDOMClassInfo_SmartCardEvent_id,
   
   // PageTransition Events
   eDOMClassInfo_PageTransitionEvent_id,
 
--- a/dom/src/base/nsDOMClassInfo.cpp
+++ b/dom/src/base/nsDOMClassInfo.cpp
@@ -1123,16 +1123,18 @@ static nsDOMClassInfoData sClassInfoData
                            ELEMENT_SCRIPTABLE_FLAGS)
 #ifdef MOZ_ENABLE_CANVAS
   NS_DEFINE_CLASSINFO_DATA(CanvasRenderingContext2D, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(CanvasGradient, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(CanvasPattern, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
+  NS_DEFINE_CLASSINFO_DATA(TextMetrics, nsDOMGenericSH,
+                           DOM_DEFAULT_SCRIPTABLE_FLAGS)
 #endif // MOZ_ENABLE_CANVAS
 
   NS_DEFINE_CLASSINFO_DATA(SmartCardEvent, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(PageTransitionEvent, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
   NS_DEFINE_CLASSINFO_DATA(WindowUtils, nsDOMGenericSH,
                            DOM_DEFAULT_SCRIPTABLE_FLAGS)
@@ -3219,16 +3221,20 @@ nsDOMClassInfo::Init()
 
   DOM_CLASSINFO_MAP_BEGIN(CanvasGradient, nsIDOMCanvasGradient)
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMCanvasGradient)
   DOM_CLASSINFO_MAP_END
 
   DOM_CLASSINFO_MAP_BEGIN(CanvasPattern, nsIDOMCanvasPattern)
     DOM_CLASSINFO_MAP_ENTRY(nsIDOMCanvasPattern)
   DOM_CLASSINFO_MAP_END
+
+  DOM_CLASSINFO_MAP_BEGIN(TextMetrics, nsIDOMTextMetrics)
+    DOM_CLASSINFO_MAP_ENTRY(nsIDOMTextMetrics)
+  DOM_CLASSINFO_MAP_END
 #endif // MOZ_ENABLE_CANVAS
 
   DOM_CLASSINFO_MAP_BEGIN(XSLTProcessor, nsIXSLTProcessor)
     DOM_CLASSINFO_MAP_ENTRY(nsIXSLTProcessor)
     DOM_CLASSINFO_MAP_ENTRY(nsIXSLTProcessorObsolete) // XXX DEPRECATED
     DOM_CLASSINFO_MAP_ENTRY(nsIXSLTProcessorPrivate)
   DOM_CLASSINFO_MAP_END
 
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/reftest.list
@@ -0,0 +1,27 @@
+!= text-ltr-left.html text-blank.html
+!= text-ltr-right.html text-blank.html
+!= text-rtl-left.html text-blank.html
+!= text-rtl-right.html text-blank.html
+
+== text-ltr-start.html text-ltr-left.html
+== text-ltr-end.html text-ltr-right.html
+!= text-ltr-left.html text-ltr-right.html
+== text-rtl-start.html text-rtl-right.html
+== text-rtl-end.html text-rtl-left.html
+!= text-rtl-left.html text-rtl-right.html
+
+!= text-ltr-left.html text-rtl-left.html
+
+== text-ltr-alignment-test.html text-ltr-alignment-ref.html
+== text-rtl-alignment-test.html text-rtl-alignment-ref.html
+
+== text-horzline-with-bottom.html text-horzline.html
+== text-horzline-with-top.html text-horzline.html
+
+!= text-big-stroke.html text-blank.html
+!= text-big-stroke.html text-big-fill.html
+
+== text-context-state-test.html text-context-state-ref.html
+== text-font-inherit.html text-big-fill.html
+== text-space-replace-test.html text-space-replace-ref.html
+
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-big-fill.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure fillText and strokeText look different</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.strokeStyle = 'black';
+  ctx.font = 'bold 64px sans-serif';
+  ctx.textAlign = 'left';
+  ctx.textBaseline = 'top';
+  ctx.fillText(str, 0, 0);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-big-stroke.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure fillText and strokeText look different</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.strokeStyle = 'black';
+  ctx.font = 'bold 64px sans-serif';
+  ctx.textAlign = 'left';
+  ctx.textBaseline = 'top';
+  ctx.strokeText(str, 0, 0);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-blank.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Blank Canvas</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-context-state-ref.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>text to ensure text attributes are saved in the context state</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+  
+  ctx.textAlign = 'left';
+  ctx.textBaseline = 'top';
+  ctx.font = '16px sans-serif';
+
+  ctx.fillText('yet another thing', 0, 0);
+
+  ctx.textAlign = 'right';
+  ctx.textBaseline = 'alphabetic';
+  ctx.font = '24px serif';
+
+  ctx.fillText('other thing', 128, 40);
+
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'bottom';
+  ctx.font = '10px sans-serif';
+
+  ctx.fillText('something', 80, 60);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-context-state-test.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>text to ensure text attributes are saved in the context state</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+  
+  ctx.textAlign = 'left';
+  ctx.textBaseline = 'top';
+  ctx.font = '16px sans-serif';
+
+  ctx.save();
+
+  ctx.textAlign = 'right';
+  ctx.textBaseline = 'alphabetic';
+  ctx.font = '24px serif';
+
+  ctx.save();
+
+  ctx.textAlign = 'center';
+  ctx.textBaseline = 'bottom';
+  ctx.font = '10px sans-serif';
+
+  ctx.fillText('something', 80, 60);
+
+  ctx.restore();
+
+  ctx.fillText('other thing', 128, 40);
+
+  ctx.restore();
+
+  ctx.fillText('yet another thing', 0, 0);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-font-inherit.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure font inherits CSS values</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="font:32px sans-serif;"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.font = 'bold 2em sans-serif';
+  ctx.textAlign = 'left';
+  ctx.textBaseline = 'top';
+  ctx.fillText(str, 0, 0);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-horzline-with-bottom.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure bottom basline-anchored text doesn't intersect horz line</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  ctx.strokeStyle = 'black';
+  ctx.beginPath();
+  ctx.moveTo(0,32);
+  ctx.lineTo(128,32);
+  ctx.stroke();
+
+  ctx.fillStyle = 'white';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'bottom';
+  ctx.fillText('TEXT', 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-horzline-with-top.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure top basline-anchored text doesn't intersect horz line</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  ctx.strokeStyle = 'black';
+  ctx.beginPath();
+  ctx.moveTo(0,32);
+  ctx.lineTo(128,32);
+  ctx.stroke();
+
+  ctx.fillStyle = 'white';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'top';
+  ctx.fillText('TEXT', 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-horzline.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Horizontal Line</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  ctx.strokeStyle = 'black';
+  ctx.beginPath();
+  ctx.moveTo(0,32);
+  ctx.lineTo(128,32);
+  ctx.stroke();
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-alignment-ref.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure left/right and start/end are offset by text width for ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'top';
+  
+  var str = 'TEXT';
+  var wid = ctx.measureText(str).width;
+
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64, 0);
+  ctx.textAlign = 'start';
+  ctx.fillText(str, 64, 20);
+  ctx.textAlign = 'center';
+  ctx.fillText(str, 64, 40);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-alignment-test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure left/right and start/end are offset by text width for ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'top';
+  
+  var str = 'TEXT';
+  var wid = ctx.measureText(str).width;
+
+  ctx.textAlign = 'right';
+  ctx.fillText(str, 64+wid, 0);
+  ctx.textAlign = 'end';
+  ctx.fillText(str, 64+wid, 20);
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64-wid/2, 40);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-end.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>end-aligned ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'end';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-left.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>left-aligned ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-right.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>right-aligned ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'right';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-ltr-start.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>start-aligned ltr text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'start';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-alignment-ref.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure left/right and start/end are offset by text width for rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'top';
+  
+  var str = 'TEXT';
+  var wid = ctx.measureText(str).width;
+
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64, 0);
+  ctx.textAlign = 'start';
+  ctx.fillText(str, 64, 20);
+  ctx.textAlign = 'center';
+  ctx.fillText(str, 64, 40);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-alignment-test.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure left/right and start/end are offset by text width for rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.textBaseline = 'top';
+  
+  var str = 'TEXT';
+  var wid = ctx.measureText(str).width;
+
+  ctx.textAlign = 'right';
+  ctx.fillText(str, 64+wid, 0);
+  ctx.textAlign = 'end';
+  ctx.fillText(str, 64-wid, 20);
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64-wid/2, 40);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-end.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>end-aligned rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'end';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-left.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>left-aligned rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'left';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-right.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>right-aligned rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'right';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-rtl-start.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>start-aligned rtl text</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:rtl"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'text';
+  ctx.fillStyle = 'black';
+  ctx.textAlign = 'start';
+  ctx.fillText(str, 64, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-space-replace-ref.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure whitespace characters are replaced with the space character</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'a b c d e f';
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.fillText(str, 0, 32);
+
+</script>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/canvas/text-space-replace-test.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test to ensure whitespace characters are replaced with the space character</title>
+</head>
+<body>
+<canvas id="c" width="128" height="64" style="direction:ltr"></canvas>
+<script type="text/javascript">
+  var canvas = document.getElementById('c');
+  var ctx = canvas.getContext('2d');
+
+  var str = 'a\x09b\x0Ac\x0Bd\x0Ce\x0Df';
+  ctx.fillStyle = 'black';
+  ctx.font = '20px sans-serif';
+  ctx.fillText(str, 0, 32);
+
+</script>
+</body>
+</html>
--- a/layout/reftests/reftest.list
+++ b/layout/reftests/reftest.list
@@ -86,8 +86,12 @@ include percent-overflow-sizing/reftest.
 
 # forms
 include forms/reftest.list
 
 # mathml
 include mathml/reftest.list
 
 include ../xul/base/src/grid/reftests/reftest.list
+
+# canvas 2D
+include canvas/reftest.list
+