1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * SVG <pattern> implementation
4 *
5 * Author:
6 * Lauris Kaplinski <lauris@kaplinski.com>
7 * bulia byak <buliabyak@users.sf.net>
8 * Jon A. Cruz <jon@joncruz.org>
9 * Abhishek Sharma
10 *
11 * Copyright (C) 2002 Lauris Kaplinski
12 *
13 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
14 */
15
16 #include "sp-pattern.h"
17
18 #include <cstring>
19 #include <string>
20
21 #include <glibmm.h>
22
23 #include <2geom/transforms.h>
24
25 #include "attributes.h"
26 #include "bad-uri-exception.h"
27 #include "document.h"
28
29 #include "sp-defs.h"
30 #include "sp-factory.h"
31 #include "sp-item.h"
32
33 #include "display/cairo-utils.h"
34 #include "display/drawing-context.h"
35 #include "display/drawing-surface.h"
36 #include "display/drawing.h"
37 #include "display/drawing-group.h"
38
39 #include "svg/svg.h"
40
SPPattern()41 SPPattern::SPPattern()
42 : SPPaintServer()
43 , SPViewBox()
44 {
45 this->ref = new SPPatternReference(this);
46 this->ref->changedSignal().connect(sigc::mem_fun(this, &SPPattern::_onRefChanged));
47
48 this->_pattern_units = UNITS_OBJECTBOUNDINGBOX;
49 this->_pattern_units_set = false;
50
51 this->_pattern_content_units = UNITS_USERSPACEONUSE;
52 this->_pattern_content_units_set = false;
53
54 this->_pattern_transform = Geom::identity();
55 this->_pattern_transform_set = false;
56
57 this->_x.unset();
58 this->_y.unset();
59 this->_width.unset();
60 this->_height.unset();
61 }
62
63 SPPattern::~SPPattern() = default;
64
build(SPDocument * doc,Inkscape::XML::Node * repr)65 void SPPattern::build(SPDocument *doc, Inkscape::XML::Node *repr)
66 {
67 SPPaintServer::build(doc, repr);
68
69 this->readAttr(SPAttr::PATTERNUNITS);
70 this->readAttr(SPAttr::PATTERNCONTENTUNITS);
71 this->readAttr(SPAttr::PATTERNTRANSFORM);
72 this->readAttr(SPAttr::X);
73 this->readAttr(SPAttr::Y);
74 this->readAttr(SPAttr::WIDTH);
75 this->readAttr(SPAttr::HEIGHT);
76 this->readAttr(SPAttr::VIEWBOX);
77 this->readAttr(SPAttr::PRESERVEASPECTRATIO);
78 this->readAttr(SPAttr::XLINK_HREF);
79 this->readAttr(SPAttr::STYLE);
80
81 /* Register ourselves */
82 doc->addResource("pattern", this);
83 }
84
release()85 void SPPattern::release()
86 {
87 if (this->document) {
88 // Unregister ourselves
89 this->document->removeResource("pattern", this);
90 }
91
92 if (this->ref) {
93 this->_modified_connection.disconnect();
94 this->ref->detach();
95 delete this->ref;
96 this->ref = nullptr;
97 }
98
99 SPPaintServer::release();
100 }
101
set(SPAttr key,const gchar * value)102 void SPPattern::set(SPAttr key, const gchar *value)
103 {
104 switch (key) {
105 case SPAttr::PATTERNUNITS:
106 if (value) {
107 if (!strcmp(value, "userSpaceOnUse")) {
108 this->_pattern_units = UNITS_USERSPACEONUSE;
109 }
110 else {
111 this->_pattern_units = UNITS_OBJECTBOUNDINGBOX;
112 }
113
114 this->_pattern_units_set = true;
115 }
116 else {
117 this->_pattern_units_set = false;
118 }
119
120 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
121 break;
122
123 case SPAttr::PATTERNCONTENTUNITS:
124 if (value) {
125 if (!strcmp(value, "userSpaceOnUse")) {
126 this->_pattern_content_units = UNITS_USERSPACEONUSE;
127 }
128 else {
129 this->_pattern_content_units = UNITS_OBJECTBOUNDINGBOX;
130 }
131
132 this->_pattern_content_units_set = true;
133 }
134 else {
135 this->_pattern_content_units_set = false;
136 }
137
138 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
139 break;
140
141 case SPAttr::PATTERNTRANSFORM: {
142 Geom::Affine t;
143
144 if (value && sp_svg_transform_read(value, &t)) {
145 this->_pattern_transform = t;
146 this->_pattern_transform_set = true;
147 }
148 else {
149 this->_pattern_transform = Geom::identity();
150 this->_pattern_transform_set = false;
151 }
152
153 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
154 break;
155 }
156 case SPAttr::X:
157 this->_x.readOrUnset(value);
158 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
159 break;
160
161 case SPAttr::Y:
162 this->_y.readOrUnset(value);
163 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
164 break;
165
166 case SPAttr::WIDTH:
167 this->_width.readOrUnset(value);
168 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
169 break;
170
171 case SPAttr::HEIGHT:
172 this->_height.readOrUnset(value);
173 this->requestModified(SP_OBJECT_MODIFIED_FLAG);
174 break;
175
176 case SPAttr::VIEWBOX:
177 set_viewBox(value);
178 this->requestModified(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG);
179 break;
180
181 case SPAttr::PRESERVEASPECTRATIO:
182 set_preserveAspectRatio(value);
183 this->requestDisplayUpdate(SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_VIEWPORT_MODIFIED_FLAG);
184 break;
185
186 case SPAttr::XLINK_HREF:
187 if (value && this->href == value) {
188 /* Href unchanged, do nothing. */
189 }
190 else {
191 this->href.clear();
192
193 if (value) {
194 // First, set the href field; it's only used in the "unchanged" check above.
195 this->href = value;
196 // Now do the attaching, which emits the changed signal.
197 if (value) {
198 try {
199 this->ref->attach(Inkscape::URI(value));
200 }
201 catch (Inkscape::BadURIException &e) {
202 g_warning("%s", e.what());
203 this->ref->detach();
204 }
205 }
206 else {
207 this->ref->detach();
208 }
209 }
210 }
211 break;
212
213 default:
214 SPPaintServer::set(key, value);
215 break;
216 }
217 }
218
219
220 /* TODO: do we need a ::remove_child handler? */
221
222 /* fixme: We need ::order_changed handler too (Lauris) */
223
_getChildren(std::list<SPObject * > & l)224 void SPPattern::_getChildren(std::list<SPObject *> &l)
225 {
226 for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
227 if (pat_i->firstChild()) { // find the first one with children
228 for (auto& child: pat_i->children) {
229 l.push_back(&child);
230 }
231 break; // do not go further up the chain if children are found
232 }
233 }
234 }
235
update(SPCtx * ctx,unsigned int flags)236 void SPPattern::update(SPCtx *ctx, unsigned int flags)
237 {
238 if (flags & SP_OBJECT_MODIFIED_FLAG) {
239 flags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
240 }
241
242 flags &= SP_OBJECT_MODIFIED_CASCADE;
243
244 std::list<SPObject *> l;
245 _getChildren(l);
246
247 for (auto child : l) {
248 sp_object_ref(child, nullptr);
249
250 if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
251 child->updateDisplay(ctx, flags);
252 }
253
254 sp_object_unref(child, nullptr);
255 }
256 }
257
modified(unsigned int flags)258 void SPPattern::modified(unsigned int flags)
259 {
260 if (flags & SP_OBJECT_MODIFIED_FLAG) {
261 flags |= SP_OBJECT_PARENT_MODIFIED_FLAG;
262 }
263
264 flags &= SP_OBJECT_MODIFIED_CASCADE;
265
266 std::list<SPObject *> l;
267 _getChildren(l);
268
269 for (auto child : l) {
270 sp_object_ref(child, nullptr);
271
272 if (flags || (child->mflags & (SP_OBJECT_MODIFIED_FLAG | SP_OBJECT_CHILD_MODIFIED_FLAG))) {
273 child->emitModified(flags);
274 }
275
276 sp_object_unref(child, nullptr);
277 }
278 }
279
_onRefChanged(SPObject * old_ref,SPObject * ref)280 void SPPattern::_onRefChanged(SPObject *old_ref, SPObject *ref)
281 {
282 if (old_ref) {
283 _modified_connection.disconnect();
284 }
285
286 if (SP_IS_PATTERN(ref)) {
287 _modified_connection = ref->connectModified(sigc::mem_fun(this, &SPPattern::_onRefModified));
288 }
289
290 _onRefModified(ref, 0);
291 }
292
_onRefModified(SPObject *,guint)293 void SPPattern::_onRefModified(SPObject * /*ref*/, guint /*flags*/)
294 {
295 requestModified(SP_OBJECT_MODIFIED_FLAG);
296 // Conditional to avoid causing infinite loop if there's a cycle in the href chain.
297 }
298
_countHrefs(SPObject * o) const299 guint SPPattern::_countHrefs(SPObject *o) const
300 {
301 if (!o)
302 return 1;
303
304 guint i = 0;
305
306 SPStyle *style = o->style;
307 if (style && style->fill.isPaintserver() && SP_IS_PATTERN(SP_STYLE_FILL_SERVER(style)) &&
308 SP_PATTERN(SP_STYLE_FILL_SERVER(style)) == this) {
309 i++;
310 }
311 if (style && style->stroke.isPaintserver() && SP_IS_PATTERN(SP_STYLE_STROKE_SERVER(style)) &&
312 SP_PATTERN(SP_STYLE_STROKE_SERVER(style)) == this) {
313 i++;
314 }
315
316 for (auto& child: o->children) {
317 i += _countHrefs(&child);
318 }
319
320 return i;
321 }
322
_chain() const323 SPPattern *SPPattern::_chain() const
324 {
325 Inkscape::XML::Document *xml_doc = document->getReprDoc();
326 Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr();
327
328 Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern");
329 repr->setAttribute("inkscape:collect", "always");
330 Glib::ustring parent_ref = Glib::ustring::compose("#%1", getRepr()->attribute("id"));
331 repr->setAttribute("xlink:href", parent_ref);
332
333 defsrepr->addChild(repr, nullptr);
334 SPObject *child = document->getObjectByRepr(repr);
335 assert(child == document->getObjectById(repr->attribute("id")));
336 g_assert(SP_IS_PATTERN(child));
337
338 return SP_PATTERN(child);
339 }
340
clone_if_necessary(SPItem * item,const gchar * property)341 SPPattern *SPPattern::clone_if_necessary(SPItem *item, const gchar *property)
342 {
343 SPPattern *pattern = this;
344 if (pattern->href.empty() || pattern->hrefcount > _countHrefs(item)) {
345 pattern = _chain();
346 Glib::ustring href = Glib::ustring::compose("url(#%1)", pattern->getRepr()->attribute("id"));
347
348 SPCSSAttr *css = sp_repr_css_attr_new();
349 sp_repr_css_set_property(css, property, href.c_str());
350 sp_repr_css_change_recursive(item->getRepr(), css, "style");
351 }
352 return pattern;
353 }
354
transform_multiply(Geom::Affine postmul,bool set)355 void SPPattern::transform_multiply(Geom::Affine postmul, bool set)
356 {
357 // this formula is for a different interpretation of pattern transforms as described in (*) in sp-pattern.cpp
358 // for it to work, we also need sp_object_read_attr( item, "transform");
359 // pattern->patternTransform = premul * item->transform * pattern->patternTransform * item->transform.inverse() *
360 // postmul;
361
362 // otherwise the formula is much simpler
363 if (set) {
364 _pattern_transform = postmul;
365 }
366 else {
367 _pattern_transform = getTransform() * postmul;
368 }
369 _pattern_transform_set = true;
370
371 setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(_pattern_transform));
372 }
373
produce(const std::vector<Inkscape::XML::Node * > & reprs,Geom::Rect bounds,SPDocument * document,Geom::Affine transform,Geom::Affine move)374 const gchar *SPPattern::produce(const std::vector<Inkscape::XML::Node *> &reprs, Geom::Rect bounds,
375 SPDocument *document, Geom::Affine transform, Geom::Affine move)
376 {
377 Inkscape::XML::Document *xml_doc = document->getReprDoc();
378 Inkscape::XML::Node *defsrepr = document->getDefs()->getRepr();
379
380 Inkscape::XML::Node *repr = xml_doc->createElement("svg:pattern");
381 repr->setAttribute("patternUnits", "userSpaceOnUse");
382 sp_repr_set_svg_double(repr, "width", bounds.dimensions()[Geom::X]);
383 sp_repr_set_svg_double(repr, "height", bounds.dimensions()[Geom::Y]);
384 repr->setAttributeOrRemoveIfEmpty("patternTransform", sp_svg_transform_write(transform));
385 defsrepr->appendChild(repr);
386 const gchar *pat_id = repr->attribute("id");
387 SPObject *pat_object = document->getObjectById(pat_id);
388
389 for (auto node : reprs) {
390 SPItem *copy = SP_ITEM(pat_object->appendChildRepr(node));
391
392 Geom::Affine dup_transform;
393 if (!sp_svg_transform_read(node->attribute("transform"), &dup_transform))
394 dup_transform = Geom::identity();
395 dup_transform *= move;
396
397 copy->doWriteTransform(dup_transform, nullptr, false);
398 }
399
400 Inkscape::GC::release(repr);
401 return pat_id;
402 }
403
rootPattern()404 SPPattern *SPPattern::rootPattern()
405 {
406 for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
407 if (pat_i->firstChild()) { // find the first one with children
408 return pat_i;
409 }
410 }
411 return this; // document is broken, we can't get to root; but at least we can return pat which is supposedly a valid
412 // pattern
413 }
414
415
416
417 // Access functions that look up fields up the chain of referenced patterns and return the first one which is set
418 // FIXME: all of them must use chase_hrefs the same as in SPGradient, to avoid lockup on circular refs
419
patternUnits() const420 SPPattern::PatternUnits SPPattern::patternUnits() const
421 {
422 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
423 if (pat_i->_pattern_units_set)
424 return pat_i->_pattern_units;
425 }
426 return _pattern_units;
427 }
428
patternContentUnits() const429 SPPattern::PatternUnits SPPattern::patternContentUnits() const
430 {
431 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
432 if (pat_i->_pattern_content_units_set)
433 return pat_i->_pattern_content_units;
434 }
435 return _pattern_content_units;
436 }
437
getTransform() const438 Geom::Affine const &SPPattern::getTransform() const
439 {
440 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
441 if (pat_i->_pattern_transform_set)
442 return pat_i->_pattern_transform;
443 }
444 return _pattern_transform;
445 }
446
x() const447 gdouble SPPattern::x() const
448 {
449 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
450 if (pat_i->_x._set)
451 return pat_i->_x.computed;
452 }
453 return 0;
454 }
455
y() const456 gdouble SPPattern::y() const
457 {
458 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
459 if (pat_i->_y._set)
460 return pat_i->_y.computed;
461 }
462 return 0;
463 }
464
width() const465 gdouble SPPattern::width() const
466 {
467 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
468 if (pat_i->_width._set)
469 return pat_i->_width.computed;
470 }
471 return 0;
472 }
473
height() const474 gdouble SPPattern::height() const
475 {
476 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
477 if (pat_i->_height._set)
478 return pat_i->_height.computed;
479 }
480 return 0;
481 }
482
viewbox() const483 Geom::OptRect SPPattern::viewbox() const
484 {
485 Geom::OptRect viewbox;
486 for (SPPattern const *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
487 if (pat_i->viewBox_set) {
488 viewbox = pat_i->viewBox;
489 break;
490 }
491 }
492 return viewbox;
493 }
494
_hasItemChildren() const495 bool SPPattern::_hasItemChildren() const
496 {
497 for (auto& child: children) {
498 if (SP_IS_ITEM(&child)) {
499 return true;
500 }
501 }
502
503 return false;
504 }
505
isValid() const506 bool SPPattern::isValid() const
507 {
508 double tile_width = width();
509 double tile_height = height();
510
511 if (tile_width <= 0 || tile_height <= 0)
512 return false;
513 return true;
514 }
515
pattern_new(cairo_t * base_ct,Geom::OptRect const & bbox,double opacity)516 cairo_pattern_t *SPPattern::pattern_new(cairo_t *base_ct, Geom::OptRect const &bbox, double opacity)
517 {
518
519 bool needs_opacity = (1.0 - opacity) >= 1e-3;
520 bool visible = opacity >= 1e-3;
521
522 if (!visible) {
523 return nullptr;
524 }
525
526 /* Show items */
527 SPPattern *shown = nullptr;
528
529 for (SPPattern *pat_i = this; pat_i != nullptr; pat_i = pat_i->ref ? pat_i->ref->getObject() : nullptr) {
530 // find the first one with item children
531 if (pat_i && SP_IS_OBJECT(pat_i) && pat_i->_hasItemChildren()) {
532 shown = pat_i;
533 break; // do not go further up the chain if children are found
534 }
535 }
536
537 if (!shown) {
538 return cairo_pattern_create_rgba(0, 0, 0, 0);
539 }
540
541 /* Create drawing for rendering */
542 Inkscape::Drawing drawing;
543 unsigned int dkey = SPItem::display_key_new(1);
544 Inkscape::DrawingGroup *root = new Inkscape::DrawingGroup(drawing);
545 drawing.setRoot(root);
546
547 for (auto& child: shown->children) {
548 if (SP_IS_ITEM(&child)) {
549 // for each item in pattern, show it on our drawing, add to the group,
550 // and connect to the release signal in case the item gets deleted
551 Inkscape::DrawingItem *cai;
552 cai = SP_ITEM(&child)->invoke_show(drawing, dkey, SP_ITEM_SHOW_DISPLAY);
553 root->appendChild(cai);
554 }
555 }
556
557 // ****** Geometry ******
558 //
559 // * "width" and "height" determine tile size.
560 // * "viewBox" (if defined) or "patternContentUnits" determines placement of content inside
561 // tile.
562 // * "x", "y", and "patternTransform" transform tile to user space after tile is generated.
563
564 // These functions recursively search up the tree to find the values.
565 double tile_x = x();
566 double tile_y = y();
567 double tile_width = width();
568 double tile_height = height();
569 if (bbox && (patternUnits() == UNITS_OBJECTBOUNDINGBOX)) {
570 tile_x *= bbox->width();
571 tile_y *= bbox->height();
572 tile_width *= bbox->width();
573 tile_height *= bbox->height();
574 }
575
576 // Pattern size in pattern space
577 Geom::Rect pattern_tile = Geom::Rect::from_xywh(0, 0, tile_width, tile_height);
578
579 // Content to tile (pattern space)
580 Geom::Affine content2ps;
581 Geom::OptRect effective_view_box = viewbox();
582 if (effective_view_box) {
583 // viewBox to pattern server (using SPViewBox)
584 viewBox = *effective_view_box;
585 c2p.setIdentity();
586 apply_viewbox(pattern_tile);
587 content2ps = c2p;
588 }
589 else {
590
591 // Content to bbox
592 if (bbox && (patternContentUnits() == UNITS_OBJECTBOUNDINGBOX)) {
593 content2ps = Geom::Affine(bbox->width(), 0.0, 0.0, bbox->height(), 0, 0);
594 }
595 }
596
597
598 // Tile (pattern space) to user.
599 Geom::Affine ps2user = Geom::Translate(tile_x, tile_y) * getTransform();
600
601
602 // Transform of object with pattern (includes screen scaling)
603 cairo_matrix_t cm;
604 cairo_get_matrix(base_ct, &cm);
605 Geom::Affine full(cm.xx, cm.yx, cm.xy, cm.yy, 0, 0);
606
607 // The DrawingSurface class handles the mapping from "logical space"
608 // (coordinates in the rendering) to "physical space" (surface pixels).
609 // An oversampling is done as the pattern may not pixel align with the final surface.
610 // The cairo surface is created when the DrawingContext is declared.
611
612 // Oversample the pattern
613 // TODO: find optimum value
614 // TODO: this is lame. instead of using descrim(), we should extract
615 // the scaling component from the complete matrix and use it
616 // to find the optimum tile size for rendering
617 // c is number of pixels in buffer x and y.
618 // Scale factor of 1.1 is too small... see bug #1251039
619 Geom::Point c(pattern_tile.dimensions() * ps2user.descrim() * full.descrim() * 2.0);
620
621 // Create drawing surface with size of pattern tile (in pattern space) but with number of pixels
622 // based on required resolution (c).
623 Inkscape::DrawingSurface pattern_surface(pattern_tile, c.ceil());
624 Inkscape::DrawingContext dc(pattern_surface);
625
626 pattern_tile *= pattern_surface.drawingTransform();
627 Geom::IntRect one_tile = pattern_tile.roundOutwards();
628
629 // Render pattern.
630 if (needs_opacity) {
631 dc.pushGroup(); // this group is for pattern + opacity
632 }
633
634 // TODO: make sure there are no leaks.
635 dc.transform(pattern_surface.drawingTransform().inverse());
636 root->setTransform(content2ps * pattern_surface.drawingTransform());
637 drawing.update();
638
639 // Render drawing to pattern_surface via drawing context, this calls root->render
640 // which is really DrawingItem->render().
641 drawing.render(dc, one_tile);
642 for (auto& child: shown->children) {
643 if (SP_IS_ITEM(&child)) {
644 SP_ITEM(&child)->invoke_hide(dkey);
645 }
646 }
647
648 // Uncomment to debug
649 // cairo_surface_t* raw = pattern_surface.raw();
650 // std::cout << " cairo_surface (sp-pattern): "
651 // << " width: " << cairo_image_surface_get_width( raw )
652 // << " height: " << cairo_image_surface_get_height( raw )
653 // << std::endl;
654 // std::string filename = "sp-pattern-" + (std::string)getId() + ".png";
655 // cairo_surface_write_to_png( pattern_surface.raw(), filename.c_str() );
656
657 if (needs_opacity) {
658 dc.popGroupToSource(); // pop raw pattern
659 dc.paint(opacity); // apply opacity
660 }
661
662 // Apply transformation to user space. Also compensate for oversampling.
663 Geom::Affine raw_transform = ps2user.inverse() * pattern_surface.drawingTransform();
664
665 // Cairo doesn't like large values of x0 and y0. We can replace x0 and y0 by equivalent
666 // values close to zero (since one tile on a grid is the same as another it doesn't
667 // matter which tile is used as the base tile).
668 int w = one_tile[Geom::X].extent();
669 int h = one_tile[Geom::Y].extent();
670 int m = raw_transform[4] / w;
671 int n = raw_transform[5] / h;
672 raw_transform *= Geom::Translate( -m*w, -n*h );
673
674 cairo_pattern_t *cp = cairo_pattern_create_for_surface(pattern_surface.raw());
675 ink_cairo_pattern_set_matrix(cp, raw_transform);
676 cairo_pattern_set_extend(cp, CAIRO_EXTEND_REPEAT);
677
678 return cp;
679 }
680
681 /*
682 Local Variables:
683 mode:c++
684 c-file-style:"stroustrup"
685 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
686 indent-tabs-mode:nil
687 fill-column:99
688 End:
689 */
690 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
691