Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Math optimizations #397

Merged
merged 3 commits into from
Aug 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions cairosvg/bounding_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

"""

from math import acos, atan, cos, fmod, isinf, pi, radians, sin, sqrt, tan
from math import (
acos, atan, copysign, cos, fmod, hypot, isinf, pi, radians, sin, sqrt, tan,
tau)

from .features import match_features
from .helpers import PATH_LETTERS, normalize, point, size
Expand Down Expand Up @@ -220,9 +222,7 @@ def bounding_box_text(surface, node):

def angle(bx, by):
"""Get the angle between vector (1,0) and vector (bx,by)."""
return fmod(
2 * pi + (1 if by > 0 else -1) * acos(bx / sqrt(bx * bx + by * by)),
2 * pi)
return fmod(tau + copysign(acos(bx / hypot(bx, by)), by), tau)
eumiro marked this conversation as resolved.
Show resolved Hide resolved


def bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
Expand Down Expand Up @@ -260,7 +260,7 @@ def bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
cx = cxprime * cos(phi) - cyprime * sin(phi) + (x1 + x) / 2
cy = cxprime * sin(phi) + cyprime * cos(phi) + (y1 + y) / 2

if phi == 0 or phi == pi:
if phi in (0, pi):
minx = cx - rx
tminx = angle(-rx, 0)
maxx = cx + rx
Expand All @@ -269,7 +269,7 @@ def bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
tminy = angle(0, -ry)
maxy = cy + ry
tmaxy = angle(0, ry)
elif phi == pi / 2 or phi == 3 * pi / 2:
elif phi in (pi / 2, 3 * pi / 2):
minx = cx - ry
tminx = angle(-ry, 0)
maxx = cx + ry
Expand Down Expand Up @@ -314,17 +314,13 @@ def bounding_box_elliptical_arc(x1, y1, rx, ry, phi, large, sweep, x, y):
angle1, angle2 = angle2, angle1
other_arc = True

if ((not other_arc and (angle1 > tminx or angle2 < tminx)) or
(other_arc and not (angle1 > tminx or angle2 < tminx))):
if other_arc == (angle1 <= tminx <= angle2):
minx = min(x, x1)
if ((not other_arc and (angle1 > tmaxx or angle2 < tmaxx)) or
(other_arc and not (angle1 > tmaxx or angle2 < tmaxx))):
if other_arc == (angle1 <= tmaxx <= angle2):
maxx = max(x, x1)
if ((not other_arc and (angle1 > tminy or angle2 < tminy)) or
(other_arc and not (angle1 > tminy or angle2 < tminy))):
if other_arc == (angle1 <= tminy <= angle2):
miny = min(y, y1)
if ((not other_arc and (angle1 > tmaxy or angle2 < tmaxy)) or
(other_arc and not (angle1 > tmaxy or angle2 < tmaxy))):
if other_arc == (angle1 <= tmaxy <= angle2):
maxy = max(y, y1)

return minx, miny, maxx - minx, maxy - miny
Expand Down
16 changes: 7 additions & 9 deletions cairosvg/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

import re
from math import atan2, cos, radians, sin, tan
from math import atan2, cos, hypot, radians, sin, tan

from .surface import cairo
from .url import parse_url
Expand All @@ -13,8 +13,8 @@
'mm': 1 / 25.4,
'cm': 1 / 2.54,
'in': 1,
'pt': 1 / 72.,
'pc': 1 / 6.,
'pt': 1 / 72,
'pc': 1 / 6,
'px': None,
}

Expand All @@ -29,7 +29,7 @@ class PointError(Exception):

def distance(x1, y1, x2, y2):
"""Get the distance between two points."""
return ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
return hypot(x2 - x1, y2 - y1)


def paint(value):
Expand Down Expand Up @@ -345,8 +345,7 @@ def size(surface, string, reference='xy'):
If ``reference`` is a float, it is used as reference for percentages. If it
is ``'x'``, we use the viewport width as reference. If it is ``'y'``, we
use the viewport height as reference. If it is ``'xy'``, we use
``(viewport_width ** 2 + viewport_height ** 2) ** .5 / 2 ** .5`` as
reference.
``hypot(viewport_width, viewport_height) / 2 ** .5`` as reference.

"""
if not string:
Expand All @@ -370,9 +369,8 @@ def size(surface, string, reference='xy'):
reference = surface.context_height or 0
elif reference == 'xy':
reference = (
(surface.context_width ** 2 +
surface.context_height ** 2) ** .5 /
2 ** .5)
hypot(surface.context_width, surface.context_height) / 2 ** .5
)
return float(string[:-1]) * reference / 100
elif string.endswith('em'):
return surface.font_size * float(string[:-2])
Expand Down
8 changes: 5 additions & 3 deletions cairosvg/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ def image(surface, node):
width = size(surface, node.get('width'), 'x')
height = size(surface, node.get('height'), 'y')

if image_bytes[:4] == b'\x89PNG' and not surface.map_image:
if image_bytes.startswith(b'\x89PNG') and not surface.map_image:
png_file = BytesIO(image_bytes)
elif (image_bytes[:5] in (b'<svg ', b'<?xml', b'<!DOC') or
image_bytes[:2] == b'\x1f\x8b') or b'<svg' in image_bytes:
elif (
image_bytes.startswith((b'<svg ', b'<?xml', b'<!DOC', b'\x1f\x8b'))
or b'<svg' in image_bytes
):
if 'x' in node:
del node['x']
if 'y' in node:
Expand Down
2 changes: 1 addition & 1 deletion cairosvg/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ def __init__(self, **kwargs):
if not bytestring:
bytestring = self.fetch_url(
parse_url(self.url), 'image/svg+xml')
if len(bytestring) >= 2 and bytestring[:2] == b'\x1f\x8b':
if bytestring.startswith(b'\x1f\x8b'):
bytestring = gzip.decompress(bytestring)
tree = ElementTree.fromstring(
bytestring, forbid_entities=not unsafe,
Expand Down
8 changes: 4 additions & 4 deletions cairosvg/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

"""

from math import pi, radians
from math import copysign, hypot, pi, radians

from .bounding_box import calculate_bounding_box
from .helpers import (
Expand Down Expand Up @@ -206,7 +206,7 @@ def path(surface, node):
angle = point_angle(0, 0, xe, ye)

# Put the second point onto the x axis
xe = (xe ** 2 + ye ** 2) ** .5
xe = hypot(xe, ye)
ye = 0

# Update the x radius if it is too small
Expand Down Expand Up @@ -423,7 +423,7 @@ def path(surface, node):
# Relative vertical line
y, string = (string + ' ').split(' ', 1)
old_x, old_y = current_point
angle = pi / 2 if size(surface, y, 'y') > 0 else -pi / 2
angle = copysign(pi / 2, size(surface, y, 'y'))
node.vertices.append((-angle, angle))
y = size(surface, y, 'y')
surface.context.rel_line_to(0, y)
Expand All @@ -433,7 +433,7 @@ def path(surface, node):
# Vertical line
y, string = (string + ' ').split(' ', 1)
old_x, old_y = current_point
angle = pi / 2 if size(surface, y, 'y') > old_y else -pi / 2
angle = copysign(pi / 2, size(surface, y, 'y') - old_y)
node.vertices.append((-angle, angle))
y = size(surface, y, 'y')
surface.context.line_to(old_x, y)
Expand Down
6 changes: 2 additions & 4 deletions cairosvg/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,8 @@ def rect(surface, node):
if rx == 0 or ry == 0:
surface.context.rectangle(x, y, width, height)
else:
if rx > width / 2:
rx = width / 2
if ry > height / 2:
ry = height / 2
rx = min(rx, width / 2)
ry = min(ry, height / 2)

# Inspired by Cairo Cookbook
# http://cairographics.org/cookbook/roundedrectangles/
Expand Down
6 changes: 3 additions & 3 deletions cairosvg/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def text(surface, node, draw_as_text=False):

text_anchor = node.get('text-anchor')
if text_anchor == 'middle':
x_align = - (width / 2. + x_bearing)
x_align = - (width / 2 + x_bearing)
if letter_spacing and node.text:
x_align -= (len(node.text) - 1) * letter_spacing / 2
elif text_anchor == 'end':
Expand All @@ -121,7 +121,7 @@ def text(surface, node, draw_as_text=False):
alignment_baseline = (node.get('dominant-baseline') or
node.get('alignment-baseline'))
if display_anchor == 'middle':
y_align = -height / 2.0 - y_bearing
y_align = -height / 2 - y_bearing
elif display_anchor == 'top':
y_align = -y_bearing
elif display_anchor == 'bottom':
Expand All @@ -130,7 +130,7 @@ def text(surface, node, draw_as_text=False):
alignment_baseline == 'middle'):
# TODO: This is wrong, Cairo gives no reasonable access to x-height
# information, so we use font top-to-bottom
y_align = (ascent + descent) / 2.0 - descent
y_align = (ascent + descent) / 2 - descent
elif (alignment_baseline == 'text-before-edge' or
alignment_baseline == 'before_edge' or
alignment_baseline == 'top' or
Expand Down