1 /*
2  * Copyright (C) 2008 Emweb bv, Herent, Belgium.
3  *
4  * See the LICENSE file for terms of use.
5  */
6 
7 #include "Wt/WApplication.h"
8 #include "Wt/WCanvasPaintDevice.h"
9 #include "Wt/WEnvironment.h"
10 #include "Wt/WException.h"
11 #include "Wt/WFontMetrics.h"
12 #include "Wt/WPainter.h"
13 #include "Wt/WPainterPath.h"
14 #include "Wt/WRectF.h"
15 #include "Wt/WStringStream.h"
16 #include "Wt/WWebWidget.h"
17 
18 #include "DomElement.h"
19 #include "WebUtils.h"
20 #include "ServerSideFontMetrics.h"
21 
22 #include <cmath>
23 #include <sstream>
24 
25 #ifndef M_PI
26 #define M_PI 3.14159265358979323846
27 #endif
28 
29 namespace {
30   static const double EPSILON = 1E-5;
31 }
32 
33 namespace Wt {
34 
35 namespace {
adjust360(double d)36   double adjust360(double d) {
37     if (d > 360.0)
38       return 360.0;
39     else if (d < -360.0)
40       return -360.0;
41     else
42       return d;
43   }
44 
adjustPositive360(double d)45   double adjustPositive360(double d) {
46     const double result = std::fmod(d, 360.0);
47     if (result < 0)
48       return result + 360.0;
49     else
50       return result;
51   }
52 
fequal(double d1,double d2)53   bool fequal(double d1, double d2) {
54     return std::fabs(d1 - d2) < 1E-5;
55   }
56 
defineGradient(const Wt::WGradient & gradient,std::stringstream & js)57   std::string defineGradient(const Wt::WGradient& gradient,
58 			     std::stringstream& js) {
59     std::string jsRef = "grad";
60     if (gradient.style() == Wt::GradientStyle::Linear) {
61       const WLineF& gradVec = gradient.linearGradientVector();
62       js << "var " << jsRef << " = ctx.createLinearGradient("
63 	  << gradVec.x1() << ", " << gradVec.y1() << ", "
64 	  << gradVec.x2() << ", " << gradVec.y2()
65 	  << ");";
66     } else if (gradient.style() == Wt::GradientStyle::Radial) {
67       js << "var " << jsRef << " = ctx.createRadialGradient("
68 	  << gradient.radialFocalPoint().x() << ", "
69 	  << gradient.radialFocalPoint().y() << ","
70 	  << "0, "
71 	  << gradient.radialCenterPoint().x() << ", "
72 	  << gradient.radialCenterPoint().y() << ", "
73 	  << gradient.radialRadius() << ");";
74     }
75     for (unsigned i=0; i<gradient.colorstops().size(); i++) {
76       js << jsRef << ".addColorStop("
77 	 << gradient.colorstops()[i].position() << ","
78 	 << WWebWidget::jsStringLiteral
79 	(gradient.colorstops()[i].color().cssText(true))
80 	 << ");";
81     }
82 
83     return jsRef;
84   }
85 }
86 
WCanvasPaintDevice(const WLength & width,const WLength & height,bool paintUpdate)87 WCanvasPaintDevice::WCanvasPaintDevice(const WLength& width,
88 				       const WLength& height,
89 				       bool paintUpdate)
90   : width_(width),
91     height_(height),
92     painter_(nullptr),
93     paintUpdate_(paintUpdate),
94     currentClippingEnabled_(false),
95     fontMetrics_(nullptr)
96 {
97   textMethod_ = TextMethod::Html5Text;
98 
99   WApplication *app = WApplication::instance();
100 
101   if (app) {
102     if (app->environment().agentIsChrome()) {
103       if (static_cast<unsigned int>(app->environment().agent()) <=
104 	  static_cast<unsigned int>(UserAgent::Chrome2))
105 	textMethod_ = TextMethod::DomText;
106     } else if (app->environment().agentIsGecko()) {
107       if (static_cast<unsigned int>(app->environment().agent()) <
108 	  static_cast<unsigned int>(UserAgent::Firefox3_0))
109 	textMethod_ = TextMethod::DomText;
110       else if (static_cast<unsigned int>(app->environment().agent())
111 	       < static_cast<unsigned int>(UserAgent::Firefox3_5))
112 	textMethod_ = TextMethod::MozText;
113     } else if (app->environment().agentIsSafari()) {
114       if (app->environment().agent() == UserAgent::Safari3)
115 	textMethod_ = TextMethod::DomText;
116     }
117   }
118 }
119 
~WCanvasPaintDevice()120 WCanvasPaintDevice::~WCanvasPaintDevice()
121 {
122   delete fontMetrics_;
123 }
124 
features()125 WFlags<PaintDeviceFeatureFlag> WCanvasPaintDevice::features() const
126 {
127   if (ServerSideFontMetrics::available())
128     return PaintDeviceFeatureFlag::FontMetrics;
129   else
130     return None;
131 }
132 
render(const std::string & paintedWidgetJsRef,const std::string & canvasId,DomElement * text,const std::string & updateAreasJs)133 void WCanvasPaintDevice::render(const std::string& paintedWidgetJsRef,
134                                 const std::string& canvasId,
135                                 DomElement *text,
136                                 const std::string& updateAreasJs)
137 {
138   std::string canvasVar = WT_CLASS ".getElement('" + canvasId + "')";
139   std::string paintedWidgetObjRef = paintedWidgetJsRef + ".wtObj";
140 
141   WStringStream tmp;
142 
143   tmp << ";" // Extra ; to make sure that JavaScript interprets the next thing as
144              // a separate statement (for when ; was forgotten in another doJavaScript call)
145          "(function(){";
146   tmp << "var pF=function(){";
147 
148   tmp <<
149     "if(" << canvasVar << ".getContext){";
150 
151   if (!images_.empty()) {
152     tmp << "var images=" << paintedWidgetObjRef << ".images;";
153   }
154   tmp << "var ctx=" << canvasVar << ".getContext('2d');";
155   // Older browsers don't have setLineDash
156   tmp << "if (!ctx.setLineDash) {ctx.setLineDash = function(a){};}";
157 
158   if (!paintUpdate_) {
159     tmp << "ctx.clearRect(0,0,"
160 	<< width().value() << "," << height().value() << ");";
161   }
162 
163   lastTransformWasIdentity_ = true;
164 
165   tmp << "ctx.save();" << js_.str()
166       << "ctx.restore();";
167 
168   tmp << "}";
169 
170   tmp << updateAreasJs;
171 
172   tmp << "};";
173 
174   if (!paintUpdate_) {
175     tmp << paintedWidgetObjRef << ".repaint=pF;";
176     tmp << "pF=function(){"
177         << paintedWidgetObjRef << ".repaint();"
178         << "};";
179   }
180 
181   tmp << "var o=" << paintedWidgetObjRef << ";";
182   if (!paintUpdate_)
183     tmp << "o.cancelPreloaders();";
184   tmp << "if(" << canvasVar << ".getContext){";
185   tmp << "var l=new ";
186   tmp << wApp->javaScriptClass() << "._p_.ImagePreloader([";
187 
188   for (unsigned i = 0; i < images_.size(); ++i) {
189     if (i != 0)
190       tmp << ',';
191     tmp << '\'' << images_[i] << '\'';
192   }
193 
194   tmp << "],function(images){"
195            "if (!" << paintedWidgetJsRef << ")"
196              "return;"
197            "this.done = true;"
198            "var o=" << paintedWidgetObjRef << ";"
199            "if(o.imagePreloaders.length===0||"
200              "this===o.imagePreloaders[0]){"
201              "o.images=images;"
202              "pF();"
203              "o.imagePreloaders.shift();"
204            "}else{"
205              "while(o.imagePreloaders.length>0&&"
206                    "o.imagePreloaders[0].done){"
207                "o.imagePreloaders[0].callback(o.imagePreloaders[0].images);"
208              "}"
209            "}"
210          "});"
211 	 "if(!l.done)"
212 	   "o.imagePreloaders.push(l);"
213        "}"
214        "})();";
215 
216   text->callJavaScript(tmp.str());
217 
218   for (unsigned i = 0; i < textElements_.size(); ++i)
219     text->addChild(textElements_[i]);
220 }
221 
renderPaintCommands(std::stringstream & js_target,const std::string & canvasElement)222 void WCanvasPaintDevice::renderPaintCommands(std::stringstream& js_target,
223 					     const std::string& canvasElement)
224 {
225   js_target << "var ctx=" << canvasElement << ".getContext('2d');";
226   js_target << "if (!ctx.setLineDash) {ctx.setLineDash = function(a){};}";
227   js_target << "ctx.save();" << js_.str()
228 	    << "ctx.restore();";
229 }
230 
init()231 void WCanvasPaintDevice::init()
232 {
233   lastTransformWasIdentity_ = true;
234   currentBrush_ = WBrush();
235   currentNoBrush_ = false;
236   currentPen_ = WPen();
237   currentNoPen_ = false;
238   currentPen_.setCapStyle(PenCapStyle::Flat);
239   currentShadow_ = WShadow();
240   currentFont_ = WFont();
241 
242   changeFlags_ = PainterChangeFlag::Transform |
243     PainterChangeFlag::Pen |
244     PainterChangeFlag::Brush |
245     PainterChangeFlag::Shadow |
246     PainterChangeFlag::Font;
247 }
248 
done()249 void WCanvasPaintDevice::done()
250 { }
251 
drawArc(const WRectF & rect,double startAngle,double spanAngle)252 void WCanvasPaintDevice::drawArc(const WRectF& rect, double startAngle,
253 				 double spanAngle)
254 {
255   if (rect.width() < EPSILON || rect.height() < EPSILON)
256     return;
257 
258   renderStateChanges(true);
259 
260   const double rStartAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle));
261   double rEndAngle;
262   if (spanAngle >= 360.0 || spanAngle <= -360.0) {
263     rEndAngle = rStartAngle - 2.0 * M_PI * (spanAngle > 0 ? 1.0 : -1.0);
264   } else {
265     rEndAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle - adjust360(spanAngle)));
266   }
267   const bool anticlockwise = spanAngle > 0;
268 
269   double sx, sy, r, lw;
270   if (rect.width() > rect.height()) {
271     sx = 1;
272     sy = std::max(0.005, rect.height() / rect.width());
273     r = rect.width()/2;
274   } else if (rect.width() < rect.height()) {
275     sx = std::max(0.005, rect.width() / rect.height());
276     sy = 1;
277     r = rect.height()/2;
278   } else {
279     sx = 1;
280     sy = 1;
281     r = rect.width() / 2;
282   }
283 
284   const WPen& pen = painter()->pen();
285   if (pen.style() != PenStyle::None)
286     lw = painter()->normalizedPenWidth(pen.width(), true).value()
287       * 1 / std::min(sx, sy);
288   else
289     lw = 0;
290 
291   char buf[30];
292 
293   js_ << "ctx.save();"
294       << "ctx.translate(" << Utils::round_js_str(rect.center().x(), 3, buf);
295   js_ << "," << Utils::round_js_str(rect.center().y(), 3, buf);
296   js_ << ");"
297       << "ctx.scale(" << Utils::round_js_str(sx, 3, buf);
298   js_ << "," << Utils::round_js_str(sy, 3, buf) << ");";
299   js_ << "ctx.lineWidth = " << Utils::round_js_str(lw, 3, buf) << ";"
300       << "ctx.beginPath();";
301   js_ << "ctx.arc(0,0," << Utils::round_js_str(r, 3, buf);
302   js_ << ',' << Utils::round_js_str(rStartAngle, 6, buf);
303   js_ << ',' << Utils::round_js_str(rEndAngle, 6, buf) << ',';
304   js_ << (anticlockwise ? "true" : "false") << ");";
305 
306   // restore comes before fill and stroke, otherwise the gradient will use
307   // this temporary coordinate system
308   js_ << "ctx.restore();";
309 
310   if (painter_->brush().style() != BrushStyle::None) {
311     js_ << "ctx.fill();";
312   }
313 
314   if (painter_->pen().style() != PenStyle::None) {
315     js_ << "ctx.stroke();";
316   }
317 }
318 
createImage(const std::string & imgUri)319 int WCanvasPaintDevice::createImage(const std::string& imgUri)
320 {
321   images_.push_back(imgUri);
322   return images_.size() - 1;
323 }
324 
drawImage(const WRectF & rect,const std::string & imageUri,int imgWidth,int imgHeight,const WRectF & sourceRect)325 void WCanvasPaintDevice::drawImage(const WRectF& rect,
326 				   const std::string& imageUri,
327 				   int imgWidth, int imgHeight,
328 				   const WRectF& sourceRect)
329 {
330   renderStateChanges(true);
331 
332   WApplication *app = WApplication::instance();
333   std::string imgUri;
334   if (app)
335     imgUri = app->resolveRelativeUrl(imageUri);
336 
337   int imageIndex = createImage(imgUri);
338 
339   js_ << WT_CLASS ".gfxUtils.drawImage("
340          "ctx,"
341          "images[" << imageIndex << "],"
342       << Wt::WWebWidget::jsStringLiteral(imgUri) << ','
343       << sourceRect.jsRef() << ','
344       << rect.jsRef() << ");";
345 }
346 
drawPlainPath(std::stringstream & out,const WPainterPath & path)347 void WCanvasPaintDevice::drawPlainPath(std::stringstream& out,
348 				       const WPainterPath& path)
349 {
350   char buf[30];
351 
352   out << "ctx.beginPath();";
353 
354   const std::vector<WPainterPath::Segment>& segments = path.segments();
355 
356   if (segments.size() > 0
357       && segments[0].type() != MoveTo)
358     out << "ctx.moveTo(0,0);";
359 
360   for (unsigned i = 0; i < segments.size(); ++i) {
361     const WPainterPath::Segment s = segments[i];
362 
363     switch (s.type()) {
364     case MoveTo:
365       out << "ctx.moveTo(" << Utils::round_js_str(s.x() + pathTranslation_.x(),
366 						  3, buf);
367       out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(),
368 				     3, buf) << ");";
369       break;
370     case LineTo:
371       out << "ctx.lineTo(" << Utils::round_js_str(s.x() + pathTranslation_.x(),
372 						  3, buf);
373       out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(),
374 					3, buf) << ");";
375       break;
376     case CubicC1:
377       out << "ctx.bezierCurveTo("
378 	  << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf);
379       out << ',' << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
380       break;
381     case CubicC2:
382       out << ',' << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf)
383 	  << ',';
384       out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
385       break;
386     case CubicEnd:
387       out << ',' << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf)
388 	  << ',';
389       out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf) << ");";
390       break;
391     case ArcC:
392       out << "ctx.arc(" << Utils::round_js_str(s.x() + pathTranslation_.x(), 3,
393 					       buf) << ',';
394       out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf);
395       break;
396     case ArcR:
397       out << ',' << Utils::round_js_str(std::max(0.0, s.x()), 3, buf);
398       break;
399     case ArcAngleSweep:
400       {
401         const double startAngle = s.x();
402         const double spanAngle = s.y();
403         const double rStartAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle));
404         double rEndAngle;
405         if (spanAngle >= 360.0 || spanAngle <= -360.0) {
406           rEndAngle = rStartAngle - 2.0 * M_PI * (spanAngle > 0 ? 1.0 : -1.0);
407         } else {
408           rEndAngle = WTransform::degreesToRadians(adjustPositive360(-startAngle - adjust360(spanAngle)));
409         }
410         const bool anticlockwise = spanAngle > 0;
411 
412         out << ',' << Utils::round_js_str(rStartAngle, 6, buf);
413         out << ',' << Utils::round_js_str(rEndAngle, 6, buf);
414         out << ',' << (anticlockwise ? "true" : "false") << ");";
415       }
416       break;
417     case QuadC: {
418       const double cpx = s.x();
419       const double cpy = s.y();
420       out << "ctx.quadraticCurveTo("
421           << Utils::round_js_str(cpx + pathTranslation_.x(), 3, buf) << ',';
422       out << Utils::round_js_str(cpy + pathTranslation_.y(), 3, buf);
423 
424       break;
425     }
426     case QuadEnd:
427       out << ','
428 	  << Utils::round_js_str(s.x() + pathTranslation_.x(), 3, buf) << ',';
429       out << Utils::round_js_str(s.y() + pathTranslation_.y(), 3, buf) << ");";
430     }
431   }
432 }
433 
finishPath()434 void WCanvasPaintDevice::finishPath()
435 {
436   if (!currentNoBrush_)
437     js_ << "ctx.fill();";
438 
439   if (!currentNoPen_)
440     js_ << "ctx.stroke();";
441 
442   js_ << '\n';
443 }
444 
drawPath(const WPainterPath & path)445 void WCanvasPaintDevice::drawPath(const WPainterPath& path)
446 {
447   if (path.isJavaScriptBound()) {
448     renderStateChanges(true);
449     js_ << WT_CLASS ".gfxUtils.drawPath(ctx," << path.jsRef() << ","
450       << (currentNoBrush_ ? "false" : "true") << ","
451       << (currentNoPen_ ? "false" : "true") << ");";
452   } else {
453     renderStateChanges(false);
454     drawPlainPath(js_, path);
455     finishPath();
456   }
457 }
458 
drawStencilAlongPath(const WPainterPath & stencil,const WPainterPath & path,bool softClipping)459 void WCanvasPaintDevice::drawStencilAlongPath(const WPainterPath &stencil,
460 					      const WPainterPath &path,
461 					      bool softClipping)
462 {
463   renderStateChanges(true);
464   js_ << WT_CLASS << ".gfxUtils.drawStencilAlongPath(ctx,"
465       << stencil.jsRef() << "," << path.jsRef() << ","
466       << (currentNoBrush_ ? "false" : "true") << ","
467       << (currentNoPen_ ? "false" : "true") << ","
468       << (softClipping ? "true" : "false") << ");";
469 }
470 
drawRect(const WRectF & rectangle)471 void WCanvasPaintDevice::drawRect(const WRectF& rectangle)
472 {
473   if (rectangle.isJavaScriptBound()) {
474     renderStateChanges(true);
475     js_ << WT_CLASS << ".gfxUtils.drawRect(ctx," << rectangle.jsRef() << ","
476 	<< (currentNoBrush_ ? "false" : "true") << ","
477 	<< (currentNoPen_ ? "false" : "true") << ");";
478   } else {
479     drawPath(rectangle.toPath());
480   }
481 }
482 
drawLine(double x1,double y1,double x2,double y2)483 void WCanvasPaintDevice::drawLine(double x1, double y1, double x2, double y2)
484 {
485   WPainterPath path;
486   path.moveTo(x1, y1);
487   path.lineTo(x2, y2);
488   drawPath(path);
489 }
490 
drawText(const WRectF & rect,WFlags<AlignmentFlag> flags,TextFlag textFlag,const WString & text,const WPointF * clipPoint)491 void WCanvasPaintDevice::drawText(const WRectF& rect,
492 				  WFlags<AlignmentFlag> flags,
493 				  TextFlag textFlag,
494 				  const WString& text,
495 				  const WPointF *clipPoint)
496 {
497   if (textFlag == TextFlag::WordWrap)
498     throw WException("WCanvasPaintDevice::drawText() "
499 		     "WordWrap is not supported");
500 
501   AlignmentFlag horizontalAlign = flags & AlignHorizontalMask;
502   AlignmentFlag verticalAlign = flags & AlignVerticalMask;
503 
504   if (textMethod_ != TextMethod::DomText) {
505     renderStateChanges(true);
506   }
507 
508   switch (textMethod_) {
509   case TextMethod::Html5Text:
510     {
511       js_ << WT_CLASS ".gfxUtils.drawText(ctx,"
512 	  << rect.jsRef() << ',' << flags.value() << ','
513 	  << text.jsStringLiteral();
514       if (clipPoint && painter()) {
515 	js_ << ',' << painter()->worldTransform().map(*clipPoint).jsRef();
516       }
517       js_ << ");";
518     }
519     break;
520   case TextMethod::MozText:
521     {
522       std::string x;
523 
524       switch (horizontalAlign) {
525       case AlignmentFlag::Left:
526 	x = std::to_string(rect.left());
527 	break;
528       case AlignmentFlag::Right:
529 	x = std::to_string(rect.right())
530 	  + " - ctx.mozMeasureText(" + text.jsStringLiteral() + ")";
531 	break;
532       case AlignmentFlag::Center:
533 	x = std::to_string(rect.center().x())
534 	  + " - ctx.mozMeasureText(" + text.jsStringLiteral() + ")/2";
535 	break;
536       default:
537 	break;
538       }
539 
540       double fontSize;
541       switch (painter()->font().size()) {
542       case FontSize::FixedSize:
543 	fontSize = painter()->font().sizeLength().toPixels();
544 	break;
545       default:
546 	fontSize = 16;
547       }
548 
549       double y = 0;
550       switch (verticalAlign) {
551       case AlignmentFlag::Top:
552 	y = rect.top() + fontSize * 0.75; break;
553       case AlignmentFlag::Middle:
554 	y = rect.center().y() + fontSize * 0.25; break;
555       case AlignmentFlag::Bottom:
556 	y = rect.bottom() - fontSize * 0.25 ; break;
557       default:
558 	break;
559       }
560 
561       js_ << "ctx.save();";
562       js_ << "ctx.translate(" << x << ", " << y << ");";
563       if (currentPen_.isJavaScriptBound()) {
564 	js_ << "ctx.fillStyle=" WT_CLASS ".gfxUtils.css_text(" << currentPen_.jsRef() << ".color);";
565       } else if (currentBrush_.color() != currentPen_.color() || currentBrush_.isJavaScriptBound())
566 	js_ << "ctx.fillStyle="
567 	    << WWebWidget::jsStringLiteral(currentPen_.color().cssText(true))
568 	    << ";";
569       js_ << "ctx.mozDrawText(" << text.jsStringLiteral() << ");";
570       js_ << "ctx.restore();";
571     }
572     break;
573   case TextMethod::DomText:
574     {
575       WPointF pos = painter()->combinedTransform().map(rect.topLeft());
576 
577       DomElement *e = DomElement::createNew(DomElementType::DIV);
578       e->setProperty(Property::StylePosition, "absolute");
579       e->setProperty(Property::StyleTop,
580 		     std::to_string(pos.y()) + "px");
581       e->setProperty(Property::StyleLeft,
582 		     std::to_string(pos.x()) + "px");
583       e->setProperty(Property::StyleWidth,
584 		     std::to_string(rect.width()) + "px");
585       e->setProperty(Property::StyleHeight,
586 		     std::to_string(rect.height()) + "px");
587 
588       DomElement *t = e;
589 
590       /*
591        * HTML tricks to center things vertically -- does not work on IE,
592        * (neither does canvas)
593        */
594       if (verticalAlign != AlignmentFlag::Top) {
595 	t = DomElement::createNew(DomElementType::DIV);
596 
597 	if (verticalAlign == AlignmentFlag::Middle) {
598 	  e->setProperty(Property::StyleDisplay, "table");
599 	  t->setProperty(Property::StyleDisplay, "table-cell");
600 	  t->setProperty(Property::StyleVerticalAlign, "middle");
601 	} else if (verticalAlign == AlignmentFlag::Bottom) {
602 	  t->setProperty(Property::StylePosition, "absolute");
603 	  t->setProperty(Property::StyleWidth, "100%");
604 	  t->setProperty(Property::StyleBottom, "0px");
605 	}
606       }
607 
608       t->setProperty(Property::InnerHTML,
609 		     WWebWidget::escapeText(text, true).toUTF8());
610 
611       WFont f = painter()->font();
612       f.updateDomElement(*t, false, true);
613 
614       t->setProperty(Property::StyleColor, painter()->pen().color().cssText());
615 
616       if (horizontalAlign == AlignmentFlag::Right)
617 	t->setProperty(Property::StyleTextAlign, "right");
618       else if (horizontalAlign == AlignmentFlag::Center)
619 	t->setProperty(Property::StyleTextAlign, "center");
620       else
621 	t->setProperty(Property::StyleTextAlign, "left");
622 
623       if (t != e)
624 	e->addChild(t);
625 
626       textElements_.push_back(e);
627     }
628   }
629 }
630 
drawTextOnPath(const WRectF & rect,WFlags<AlignmentFlag> alignmentFlags,const std::vector<WString> & text,const WTransform & transform,const WPainterPath & path,double angle,double lineHeight,bool softClipping)631 void WCanvasPaintDevice::drawTextOnPath(const WRectF &rect,
632 					WFlags<AlignmentFlag> alignmentFlags,
633 					const std::vector<WString> &text,
634 					const WTransform &transform,
635 					const WPainterPath &path,
636 					double angle, double lineHeight,
637 					bool softClipping)
638 {
639   renderStateChanges(true);
640   js_ << WT_CLASS ".gfxUtils.drawTextOnPath(ctx,[";
641   for (std::size_t i = 0; i < text.size(); ++i) {
642     if (i != 0)
643       js_ << ',';
644     js_ << text[i].jsStringLiteral();
645   }
646   js_ << "],";
647   js_ << rect.jsRef() << ',';
648   js_ << transform.jsRef() << ',';
649   js_ << path.jsRef() << ',';
650   char buf[30];
651   js_ << Utils::round_js_str(angle, 3, buf) << ',';
652   js_ << Utils::round_js_str(lineHeight, 3, buf) << ',';
653   js_ << alignmentFlags.value() << ',';
654   js_ << (softClipping ? "true" : "false") << ");";
655 }
656 
measureText(const WString & text,double maxWidth,bool wordWrap)657 WTextItem WCanvasPaintDevice::measureText(const WString& text, double maxWidth,
658 					  bool wordWrap)
659 {
660   if (!fontMetrics_)
661     fontMetrics_ = new ServerSideFontMetrics();
662 
663   return fontMetrics_->measureText(painter()->font(), text, maxWidth, wordWrap);
664 }
665 
fontMetrics()666 WFontMetrics WCanvasPaintDevice::fontMetrics()
667 {
668   if (!fontMetrics_)
669     fontMetrics_ = new ServerSideFontMetrics();
670 
671   return fontMetrics_->fontMetrics(painter()->font());
672 }
673 
setChanged(WFlags<PainterChangeFlag> flags)674 void WCanvasPaintDevice::setChanged(WFlags<PainterChangeFlag> flags)
675 {
676   changeFlags_ |= flags;
677 }
678 
renderTransform(std::stringstream & s,const WTransform & t)679 void WCanvasPaintDevice::renderTransform(std::stringstream& s,
680 					 const WTransform& t)
681 {
682   if (!(t.isIdentity() && lastTransformWasIdentity_)) {
683     s << "ctx.wtTransform=" << t.jsRef() << ';';
684     s << "ctx.setTransform.apply(ctx, ctx.wtTransform);";
685   }
686   lastTransformWasIdentity_ = t.isIdentity();
687 }
688 
renderStateChanges(bool resetPathTranslation)689 void WCanvasPaintDevice::renderStateChanges(bool resetPathTranslation)
690 {
691   if (resetPathTranslation) {
692     if (!fequal(pathTranslation_.x(), 0) ||
693 	!fequal(pathTranslation_.y(), 0))
694       changeFlags_ |= PainterChangeFlag::Transform;
695   }
696 
697   if (!changeFlags_)
698     return;
699 
700   /*
701    * For unclear reasons, Firefox on Linux reacts badly against very
702    * long painter paths (e.g. when drawing markers in charts) and by
703    * more prematurely rendering pen/brush changes we avoid that.
704    */
705   WApplication *app = WApplication::instance();
706   bool slowFirefox = app && app->environment().agentIsGecko();
707   if (slowFirefox &&
708       app->environment().userAgent().find("Linux") == std::string::npos)
709     slowFirefox = false;
710 
711   bool brushChanged =
712     changeFlags_.test(PainterChangeFlag::Brush) &&
713     (currentBrush_ != painter()->brush()) &&
714     (slowFirefox || painter()->brush().style() != BrushStyle::None);
715 
716   bool penChanged =
717     changeFlags_.test(PainterChangeFlag::Pen) &&
718     (currentPen_ != painter()->pen()) &&
719     (slowFirefox || painter()->pen().style() != PenStyle::None);
720 
721   bool penColorChanged =
722     penChanged &&
723     (painter()->pen().isJavaScriptBound() ||
724      currentPen_.color() != painter()->pen().color() ||
725      currentPen_.gradient() != painter()->pen().gradient());
726 
727   bool shadowChanged =
728     changeFlags_.test(PainterChangeFlag::Shadow)
729     && (currentShadow_ != painter()->shadow());
730 
731   bool fontChanged = changeFlags_.test(PainterChangeFlag::Font)
732     && (currentFont_ != painter()->font());
733 
734   bool clippingChanged = changeFlags_.test(PainterChangeFlag::Clipping)
735     && (currentClippingEnabled_ != painter()->hasClipping() ||
736 	 currentClipPath_ != painter()->clipPath() ||
737 	 currentClipTransform_ != painter()->clipPathTransform());
738 
739   changeFlags_.clear(PainterChangeFlag::Clipping);
740 
741   if (changeFlags_.test(PainterChangeFlag::Transform) || clippingChanged) {
742     bool resetTransform = false;
743 
744     if (clippingChanged) {
745       js_ << "ctx.restore();ctx.save();";
746 
747       lastTransformWasIdentity_ = true;
748       pathTranslation_.setX(0);
749       pathTranslation_.setY(0);
750 
751       const WTransform& t = painter()->clipPathTransform();
752       const WPainterPath &p = painter()->clipPath();
753       if (!p.isEmpty()) {
754 	js_ << WT_CLASS << ".gfxUtils.setClipPath(ctx," << p.jsRef() << "," << t.jsRef() << ","
755 	    << (painter()->hasClipping() ? "true" : "false") << ");";
756       } else {
757 	js_ << WT_CLASS << ".gfxUtils.removeClipPath(ctx);";
758       }
759       currentClipTransform_ = t;
760       currentClipPath_ = p;
761 
762       penChanged = true;
763       penColorChanged = true;
764       brushChanged = true;
765       shadowChanged = true;
766       fontChanged = true;
767       init();
768       resetTransform = true;
769 
770       currentClippingEnabled_ = painter()->hasClipping();
771     } else if (changeFlags_.test(PainterChangeFlag::Transform)) {
772       WTransform f = painter()->combinedTransform();
773 
774       resetTransform = currentTransform_ != f ||
775 	  ((!fequal(pathTranslation_.x(), 0) ||
776 	    !fequal(pathTranslation_.y(), 0)) && resetPathTranslation);
777 
778       if (!painter()->brush().gradient().isEmpty() ||
779 	  !painter()->pen().gradient().isEmpty()) {
780 	resetTransform = true;
781       } else if (!resetPathTranslation
782 		 && !currentTransform_.isJavaScriptBound()
783 		 && !f.isJavaScriptBound()
784 		 && fequal(f.m11(), currentTransform_.m11())
785 		 && fequal(f.m12(), currentTransform_.m12())
786 		 && fequal(f.m21(), currentTransform_.m21())
787 		 && fequal(f.m22(), currentTransform_.m22())) {
788 	/*
789 	 * Invert scale/rotate to compute the delta needed
790 	 * before applying these transformations to get the
791 	 * same as the global translation.
792 	 *
793 	 * (This is an optimization that prevents the rendering from
794 	 * involving many ctx.transform calls, when only moves
795 	 * are performed.)
796 	 */
797 	 double det = f.m11() * f.m22() - f.m12() * f.m21();
798 	 double a11 = f.m22() / det;
799 	 double a12 = -f.m12() / det;
800 	 double a21 = -f.m21() / det;
801 	 double a22 = f.m11() / det;
802 
803 	 double fdx = f.dx() * a11 + f.dy() * a21;
804 	 double fdy = f.dx() * a12 + f.dy() * a22;
805 
806 	 const WTransform& g = currentTransform_;
807 
808 	 double gdx = g.dx() * a11 + g.dy() * a21;
809 	 double gdy = g.dx() * a12 + g.dy() * a22;
810 
811 	 double dx = fdx - gdx;
812 	 double dy = fdy - gdy;
813 
814 	 pathTranslation_.setX(dx);
815 	 pathTranslation_.setY(dy);
816 
817          changeFlags_ = None;
818 
819 	 resetTransform = false;
820       }
821     }
822 
823     if (resetTransform) {
824       currentTransform_ = painter()->combinedTransform();
825       renderTransform(js_, currentTransform_);
826       pathTranslation_.setX(0);
827       pathTranslation_.setY(0);
828     }
829   }
830 
831   currentNoPen_ = painter()->pen().style() == PenStyle::None;
832   currentNoBrush_ = painter()->brush().style() == BrushStyle::None;
833 
834   if (penChanged) {
835     if (penColorChanged) {
836       // prevent infinite recursion by applying new color to a temporary pen
837       PenCapStyle capStyle = currentPen_.capStyle();
838       PenJoinStyle joinStyle = currentPen_.joinStyle();
839       WPen tmpPen;
840       tmpPen.setCapStyle(capStyle);
841       tmpPen.setJoinStyle(joinStyle);
842       tmpPen.setColor(painter()->pen().color());
843       tmpPen.setGradient(painter()->pen().gradient());
844       currentPen_ = tmpPen;
845       if (!painter()->pen().gradient().isEmpty()) {
846 	std::string gradientName = defineGradient(painter()->pen().gradient(),
847 						  js_);
848 	js_ << "ctx.strokeStyle=" << gradientName << ";";
849 	renderStateChanges(true);
850       } else {
851 	if (painter()->pen().isJavaScriptBound()) {
852 	  js_ << "ctx.strokeStyle=" WT_CLASS ".gfxUtils.css_text(" << painter()->pen().jsRef() << ".color);";
853 	} else {
854 	  js_ << "ctx.strokeStyle="
855 	      << WWebWidget::jsStringLiteral
856 	    (painter()->pen().color().cssText(true))
857 	      << ";";
858 	}
859       }
860     }
861 
862     char buf[30];
863     double lw = painter()->normalizedPenWidth(painter()->pen().width(), true).value();
864 
865     switch (painter()->pen().style()) {
866     case PenStyle::SolidLine:
867       js_ << "ctx.setLineDash([]);";
868       break;
869     case PenStyle::DashLine:
870       js_ << "ctx.setLineDash([";
871       js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
872       js_ << Utils::round_js_str(lw * 2.0, 3, buf);
873       js_ << "]);";
874       break;
875     case PenStyle::DotLine:
876       js_ << "ctx.setLineDash([";
877       js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
878       js_ << Utils::round_js_str(lw * 2.0, 3, buf);
879       js_ << "]);";
880       break;
881     case PenStyle::DashDotLine:
882       js_ << "ctx.setLineDash([";
883       js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
884       js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
885       js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
886       js_ << Utils::round_js_str(lw * 2.0, 3, buf);
887       js_ << "]);";
888       break;
889     case PenStyle::DashDotDotLine:
890       js_ << "ctx.setLineDash([";
891       js_ << Utils::round_js_str(lw * 4.0, 3, buf) << ',';
892       js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
893       js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
894       js_ << Utils::round_js_str(lw * 2.0, 3, buf) << ',';
895       js_ << Utils::round_js_str(lw * 1.0, 3, buf) << ',';
896       js_ << Utils::round_js_str(lw * 2.0, 3, buf);
897       js_ << "]);";
898       break;
899     case PenStyle::None:
900       break;
901     }
902 
903     js_ << "ctx.lineWidth="
904         << Utils::round_js_str(lw, 3, buf)
905 	<< ';';
906 
907     if (currentPen_.capStyle() != painter()->pen().capStyle())
908       switch (painter()->pen().capStyle()) {
909       case PenCapStyle::Flat:
910 	js_ << "ctx.lineCap='butt';";
911 	break;
912       case PenCapStyle::Square:
913 	js_ << "ctx.lineCap='square';";
914 	break;
915       case PenCapStyle::Round:
916 	js_ << "ctx.lineCap='round';";
917       }
918 
919     if (currentPen_.joinStyle() != painter()->pen().joinStyle())
920       switch (painter()->pen().joinStyle()) {
921       case PenJoinStyle::Miter:
922 	js_ << "ctx.lineJoin='miter';";
923 	break;
924       case PenJoinStyle::Bevel:
925 	js_ << "ctx.lineJoin='bevel';";
926 	break;
927       case PenJoinStyle::Round:
928 	js_ << "ctx.lineJoin='round';";
929       }
930 
931     currentPen_ = painter()->pen();
932   }
933 
934   if (brushChanged) {
935     currentBrush_ = painter_->brush();
936     if (!currentBrush_.gradient().isEmpty()) {
937       std::string gradientName = defineGradient(currentBrush_.gradient(), js_);
938       js_ << "ctx.fillStyle=" << gradientName << ";";
939       renderStateChanges(true);
940     } else {
941       if (currentBrush_.isJavaScriptBound()) {
942 	js_ << "ctx.fillStyle=" WT_CLASS ".gfxUtils.css_text("
943 	    << currentBrush_.jsRef() << ".color);";
944       } else {
945 	js_ << "ctx.fillStyle="
946 	    << WWebWidget::jsStringLiteral(currentBrush_.color().cssText(true))
947 	    << ";";
948       }
949     }
950   }
951 
952   if (shadowChanged) {
953     currentShadow_ = painter_->shadow();
954 
955     double offsetX = currentShadow_.offsetX();
956     double offsetY = currentShadow_.offsetY();
957     double blur = currentShadow_.blur();
958 
959     char buf[30];
960     js_ << "ctx.shadowOffsetX=" << Utils::round_js_str(offsetX, 3, buf) << ';';
961     js_ << "ctx.shadowOffsetY=" << Utils::round_js_str(offsetY, 3, buf) << ';';
962     js_ << "ctx.shadowBlur=" << Utils::round_js_str(blur, 3, buf) << ';'
963 	<< "ctx.shadowColor="
964 	<< WWebWidget::jsStringLiteral(currentShadow_.color().cssText(true))
965 	<< ";";
966   }
967 
968   if (fontChanged) {
969     currentFont_ = painter_->font();
970 
971     switch (textMethod_) {
972     case TextMethod::Html5Text:
973       js_ << "ctx.font="
974 	  << WWebWidget::jsStringLiteral(painter()->font().cssText()) << ";";
975       break;
976     case TextMethod::MozText:
977       js_ << "ctx.mozTextStyle = "
978 	  << WWebWidget::jsStringLiteral(painter()->font().cssText()) << ";";
979       break;
980     case TextMethod::DomText:
981       break;
982     }
983   }
984 
985   changeFlags_ = None;
986 }
987 
988 }
989