getScreenBBox and getBoundingClientRect

1,727 views
Skip to first unread message

~ paPus ~

unread,
Feb 6, 2011, 7:02:18 AM2/6/11
to lib-gwt-svg
Hi! :)

AFAIK, there is currently no function for getting the correct
screen bounding rectangle for an SVG element. Distorting the bounding
rectangle of the original element with getScreenCTM and getting its
extremes won't give you the extremes of the distorted element itself.
Think of a rotated circle (eg. 45 degrees) and the circle's bounding
box, rotated. The box has corners, which will expand the bounding
rectangle OF the bounding rectangle, which will be bigger than the
original elements bounding rectangle. One way around this (if you want
an exact bounding box) is to apply the transforms to the objects, if
possible. That means you will have to convert rectangles and ellipses
and circles etc to paths (or calculate the new bounds as described in
http://presentationlayer.blogspot.com/2008/09/svg-boundingbox-of-rotated-ellipse.html),
or if you have paths, go through each segment and apply the matrix to
each point. This is slow and painful.
However, there is the getScreenBBox from the SVG 1.2 specs, which
will give you this, but it's not implemented, only in Opera (which
rotates the bounding box also, I think).
Another promising function is the getBoundingClientRect, which
works for all kinds of elements. This however takes the stroke-width
and other settings into account, unlike the getBBox. The bigger
problem is, that it's not implemented correctly or uniformly in the
browsers. I get problems especially for G and text elements in
Firefox, where the bounds are very large. Chrome and Opera seem fine.
Here is how you can get the boundingClientRect on an SVG element:

public static Bounds boundingClientRect(OMSVGElement element)
{
return boundingClientRect(element.getElement());
}

private static native Bounds boundingClientRect(Element element) /
*-{
var bounds = element.ownerSVGElement.createSVGRect();

var rect = element.getBoundingClientRect();
bounds.x = rect.left;
bounds.y = rect.top;
bounds.width = rect.width;
bounds.height = rect.height;

return bounds;
}-*/;

Lukas, would it be a good idea to file a bug for this also? You have a
nice collection of bug reports (some of which are already marked as
solved or not to be solved, you may want to update that page :)).

Thanks,
Lőrinc

Lukas Laag

unread,
Feb 6, 2011, 4:17:10 PM2/6/11
to lib-g...@googlegroups.com

Hi again Lőrinc,

I have read your explanations regarding bounding box. Maybe there is
something which I do not understand, but I do not really see any bug or
shortcoming in the SVG API. I have tried to write a small code sample
for FF4 which does what I think you want to do:

1/ I create a path element which I rotate and translate
2/ I create a first bounding box (in blue) which I obtain by taking the
rect obtained by getBBox() and applying the same transform as the path
(the bounding box is thus rotated and translated in the same way as the
path)
3/ I then compute coordinates of the 4 corners of this first bounding
box in the screen coordinate system.
4/ I compute the bounding box of these 4 corners in the screen
coordinate system and use that to create a second bounding box (in red)

public class BBoxTest implements EntryPoint {
OMSVGDocument document;
OMSVGSVGElement svg;
OMSVGPathElement path1;
public void run() {
document = OMSVGParser.currentDocument();
svg = document.createSVGSVGElement();
svg.getStyle().setWidth(600, Unit.PX);
svg.getStyle().setHeight(500, Unit.PX);

path1 = document.createSVGPathElement();
OMSVGPathSegList segs = path1.getPathSegList();
segs.appendItem(path1.createSVGPathSegMovetoAbs(10, 20));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(40,
10, 20, 10, 30, 0));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(70,
20, 50, 20, 60, 30));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(90,
10, 80, 10, 90, 10));
OMSVGTransformList xforms = path1.getTransform().getBaseVal();
final OMSVGTransform r = svg.createSVGTransform();
r.setRotate(30, 50, 25);
final OMSVGTransform t = svg.createSVGTransform();
t.setTranslate(200, 100);
xforms.appendItem(r);
xforms.appendItem(t);

path1.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY,
SVGConstants.CSS_BLACK_VALUE);

path1.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY,
SVGConstants.CSS_NONE_VALUE);

svg.appendChild(path1);
svg.appendChild(path1);
RootPanel.get().add(new SVGImage(svg));
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
OMSVGRect bbox = path1.getBBox();
OMSVGRectElement rect1 =
document.createSVGRectElement(bbox.getX(), bbox.getY(), bbox.getWidth(),
bbox.getHeight(), 0, 0);
svg.appendChild(rect1);
OMSVGTransformList xforms =
rect1.getTransform().getBaseVal();
xforms.appendItem(r);
xforms.appendItem(t);

rect1.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY,
SVGConstants.CSS_BLUE_VALUE);

rect1.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY,
SVGConstants.CSS_NONE_VALUE);
svg.appendChild(rect1);

OMSVGMatrix m = path1.getScreenCTM();
List<OMSVGPoint> list = new
ArrayList<OMSVGPoint>();
list.add(svg.createSVGPoint(bbox.getX(),
bbox.getY()).matrixTransform(m));
list.add(svg.createSVGPoint(bbox.getMaxX(),
bbox.getY()).matrixTransform(m));
list.add(svg.createSVGPoint(bbox.getX(),
bbox.getMaxY()).matrixTransform(m));
list.add(svg.createSVGPoint(bbox.getMaxX(),
bbox.getMaxY()).matrixTransform(m));
OMSVGPoint upperLeft = getUpperLeft(list);
OMSVGPoint lowerRight = getLowerRight(list);
OMSVGRectElement rect2 =
document.createSVGRectElement(upperLeft.getX(), upperLeft.getY(),
lowerRight.getX() - upperLeft.getX(), lowerRight.getY() -
upperLeft.getY(), 0, 0);

rect2.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY,
SVGConstants.CSS_RED_VALUE);

rect2.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY,
SVGConstants.CSS_NONE_VALUE);
svg.appendChild(rect2);
}
});
}

private OMSVGPoint getUpperLeft(List<OMSVGPoint> list) {
OMSVGPoint upperLeft =
svg.createSVGPoint(list.get(0).getX(), list.get(0).getY());
for (OMSVGPoint p : list) {
upperLeft.setX(Math.min(p.getX(),
upperLeft.getX()));
upperLeft.setY(Math.min(p.getY(),
upperLeft.getY()));
}
return upperLeft;
}
private OMSVGPoint getLowerRight(List<OMSVGPoint> list) {
OMSVGPoint lowerRight =
svg.createSVGPoint(list.get(0).getX(), list.get(0).getY());
for (OMSVGPoint p : list) {
lowerRight.setX(Math.max(p.getX(),
lowerRight.getX()));
lowerRight.setY(Math.max(p.getY(),
lowerRight.getY()));
}
return lowerRight;
}

@Override
public void onModuleLoad() {
run();
}
}


I hope this solves your problem.

Regarding the bugs page, I try to maintain the information up to date.
If a bug is fixed, I mark it with a strikethrough font. However I do not
mention it clearly when an issue is not going to be fixed because the
browser implementors do not agree with me or do not feel that it is
worth fixing. It would be better to do it, I will add that do my todo list.

Regards

Lukas

Pap Lôrinc

unread,
Feb 6, 2011, 5:54:22 PM2/6/11
to lib-g...@googlegroups.com
Thank you Lukas for your time.

The real screen bounding box is the one in orange in the following picture:
http://img155.imageshack.us/img155/5192/bbox.png
Blue - original getBBox
Red - transformed original
Green - getBoundingClientRect
Orange - real screen bounding box

The bounding box should be touching the original element (orange), not its bounding box (red) :)
The boundingClientRect (green) has a similar problem.

I have modified the code to include it:
http://pastie.org/1535254

ps. btw, if you consolidate your transforms, the code will work in Chrome too :), as in the above pastie
ps2. in my original post the 'boundingClientRect' should return a 'OMSVGRect', not a 'Bounds', also as in the above pastie
ps3. googlegroups didn't want to allow my reply, saying "We were unable to post your message If you believe this is an error, please contact Google Support."

Lőrinc

Lukas Laag

unread,
Feb 6, 2011, 6:20:31 PM2/6/11
to lib-g...@googlegroups.com
> <https://groups.google.com/support/bin/request.py?hl=en&contact_type=bugs>."
>
> Lőrinc

A picture speaks a thousand words... Many thanks, I now understand what
you would like to compute. Alas I think most browsers officially target
SVG 1.1, not 1.2
(http://en.wikipedia.org/wiki/Comparison_of_layout_engines_%28Scalable_Vector_Graphics%29).


Your suggestion about getScreenBBox, though valid, is more of a feature
request for SVG1.2 support in browsers than an actual bug report. I am
not sure it will have much effect if I create a bug report for that.

Lukas

~ paPus ~

unread,
Feb 7, 2011, 6:16:38 AM2/7/11
to lib-gwt-svg
I know that the getScreenBBox would be a new feature, but the
getBoundingClientRect is an existing function, that behaves quite
differently across browsers. The problem is especially with Firefox
and G elements.
It would be a nice alternative for the getScreenBBox, maybe even a
fast one. It automatically includes strokes, which could be very
handy, because what you see is what you get :).
I am writing a music notation software and it spends most of the
time in the bounding rectangle calculation and the transforms,
speeding it up would be nice :).

Thanks,
Lőrinc

Lukas Laag

unread,
Feb 8, 2011, 5:26:39 AM2/8/11
to lib-g...@googlegroups.com
Hi Lőrinc,

I am exploring the 'getBoundingClientRect' branch of your request. I
have updated my earlier sample and tested it with FF4b10, Opera11 and
Chromium 11.0.658.0 (73591) to have it also display the result of
getBoundingClientRect based on the method you kindly supplied. So now
we have 3 rectangles, as per your picture:
+ blue: rotated bbox
+ red: bbox of rotated bbox in screen coordinates
+ orange: getBoundingClientRect

It seems there are two schools: Opera and Chromium give the same value
for red and orange; FF gives 3 different values. None of the three
gives the ideal bbox you have drawn in your picture. I have looked at
bug trackers and dug up two bugs:
https://bugzilla.mozilla.org/show_bug.cgi?id=530985
http://code.google.com/p/chromium/issues/detail?id=47998
So it seems browser implementors are working on the issue, but since
the method is not very formally defined, it will not be easy to reach
consensus on this one.

Lukas

public class BBoxTest implements EntryPoint {
OMSVGDocument document;
OMSVGSVGElement svg;
OMSVGPathElement path1;
public void run() {
document = OMSVGParser.currentDocument();
svg = document.createSVGSVGElement();
svg.getStyle().setWidth(600, Unit.PX);
svg.getStyle().setHeight(500, Unit.PX);

path1 = document.createSVGPathElement();
OMSVGPathSegList segs = path1.getPathSegList();
segs.appendItem(path1.createSVGPathSegMovetoAbs(10, 20));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(40, 10, 20,
10, 30, 0));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(70, 20, 50,
20, 60, 30));
segs.appendItem(path1.createSVGPathSegCurvetoCubicAbs(90, 10, 80,
10, 90, 10));

/*OMSVGTransformList xforms = path1.getTransform().getBaseVal();


final OMSVGTransform r = svg.createSVGTransform();
r.setRotate(30, 50, 25);
final OMSVGTransform t = svg.createSVGTransform();
t.setTranslate(200, 100);
xforms.appendItem(r);

xforms.appendItem(t);*/
path1.setAttribute("transform", "rotate(30,50,25) translate(200,100)");


path1.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY,
SVGConstants.CSS_BLACK_VALUE);
path1.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY,
SVGConstants.CSS_NONE_VALUE);

svg.appendChild(path1);
svg.appendChild(path1);
RootPanel.get().add(new SVGImage(svg));
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
OMSVGRect bbox = path1.getBBox();
OMSVGRectElement rect1 =
document.createSVGRectElement(bbox.getX(), bbox.getY(),
bbox.getWidth(), bbox.getHeight(), 0, 0);
svg.appendChild(rect1);

/*OMSVGTransformList xforms = rect1.getTransform().getBaseVal();
xforms.appendItem(r);
xforms.appendItem(t);*/
rect1.setAttribute("transform", "rotate(30,50,25) translate(200,100)");

OMSVGRectElement rect3 =
document.createSVGRectElement(boundingClientRect((SVGElement)path1.getElement().cast()));
rect3.getStyle().setSVGProperty(SVGConstants.CSS_STROKE_PROPERTY,
SVGConstants.CSS_ORANGE_VALUE);
rect3.getStyle().setSVGProperty(SVGConstants.CSS_FILL_PROPERTY,
SVGConstants.CSS_NONE_VALUE);
svg.appendChild(rect3);

}
});
}

private static native OMSVGRect boundingClientRect(SVGElement element) /*-{
var bounds = element.ownerSVGElement.createSVGRect();

var rect = element.getBoundingClientRect();
bounds.x = rect.left;
bounds.y = rect.top;
bounds.width = rect.width;
bounds.height = rect.height;

return bounds;
}-*/;


private OMSVGPoint getUpperLeft(List<OMSVGPoint> list) {

OMSVGPoint upperLeft = svg.createSVGPoint(list.get(0));


for (OMSVGPoint p : list) {
upperLeft.setX(Math.min(p.getX(), upperLeft.getX()));
upperLeft.setY(Math.min(p.getY(), upperLeft.getY()));
}
return upperLeft;
}
private OMSVGPoint getLowerRight(List<OMSVGPoint> list) {

OMSVGPoint lowerRight = svg.createSVGPoint(list.get(0));


for (OMSVGPoint p : list) {
lowerRight.setX(Math.max(p.getX(), lowerRight.getX()));
lowerRight.setY(Math.max(p.getY(), lowerRight.getY()));
}
return lowerRight;
}

@Override
public void onModuleLoad() {
run();
}
}

Reply all
Reply to author
Forward
0 new messages