#!/usr/bin/env python

"""Testsuite for svglib.

This tests basic functionality. Run with one of these lines from
inside the test directory:

    py.test -v -s test_basic.py
"""
import unittest
from reportlab.lib.testutils import makeSuiteForClasses
import io, logging, os
import textwrap
from rlextra.thirdparty.svg import svglib
try:
    from lxml import etree
    def parseXML(s):
        return svglib.NodeCompat(parseXML = etree.XML)
except ImportError:
    import xml.dom.minidom 
    def parseXML(s):
        return svglib.NodeCompat(xml.dom.minidom.parseString(s).documentElement)

from reportlab.graphics.shapes import (
    _CLOSEPATH, _CURVETO, _LINETO, _MOVETO, Group, Path, Polygon, PolyLine, Rect,
    )
from reportlab.lib import colors
from reportlab.lib.units import cm, inch
from reportlab.pdfgen.canvas import FILL_EVEN_ODD

svglib.logger.addHandler(logging.StreamHandler(open(os.path.join(os.path.dirname(__file__),'text_svg_basic.log'),'w')))

def _testit(func, mapping):
    "Call `func` on input in mapping and return list of failed tests."

    failed = []
    for input, expected in mapping:
        result = func(input)
        if not result == expected:
            failed.append((input, result, expected))

    if failed:
        print("failed tests (input, result, expected):")
        for input, result, expected in failed:
            print("  %s : %s != %s" % (repr(input), result, expected))

    return failed


class SVGBasicTest(unittest.TestCase):
    pass

class TestPaths(SVGBasicTest):
    """Testing path-related code."""

    def test_path_normalisation(self):
        "Test path normalisation."

        mapping = (
            ("",
                []),

            ("M10 20, L 30 40 ",
                ["M", [10, 20], "L", [30, 40]]),

            ("M10 20, L 40 40Z",
                ["M", [10, 20], "L", [40, 40], "Z", []]),

            ("M10 20, L 30 40 40 40Z",
                ["M", [10, 20], "L", [30, 40], "L", [40, 40], "Z", []]),

            ("  M10 20,L30 40,40 40Z  ",
                ["M", [10, 20], "L", [30, 40], "L", [40, 40], "Z", []]),

            ("  M 10 20, M 30 40, L 40 40, Z M 40 50, l 50 60, Z",
                ["M", [10, 20], "L", [30, 40], "L", [40, 40], "Z", [],
                 "M", [40, 50], "l", [50, 60], "Z", []]),

            ("  m 10 20, m 30 40, l 40 40, z",
                ["m", [10, 20], "l", [30, 40], "l", [40, 40], "z", []]),

            ("  m 10,20 30,40, l 40 40, z ",
                ["m", [10, 20], "l", [30, 40], "l", [40, 40], "z", []]),

            ("M 10,20 30,40, l 40 40, z",
                ["M", [10, 20], "L", [30, 40], "l", [40, 40], "z", []]),

            ("M0,0 500,300M500,0 0,300",
                ["M", [0, 0], "L", [500, 300], "M", [500, 0], "L", [0, 300]]),

            ("M10 20, l 5e-5,0",
                ["M", [10, 20], "l", [5e-5, 0]]),

            ("m246.026 120.178c-.558-.295-1.186-.768-1.395-1.054-.314-.438-.132-.456 1.163-.104 "
             "2.318.629 3.814.383 5.298-.873l1.308-1.103 1.54.784c.848.428 1.748.725 "
             "2.008.656.667-.176 2.05-1.95 2.005-2.564-.054-.759.587-.568.896.264.615 1.631-.281 "
             "3.502-1.865 3.918-.773.201-1.488.127-2.659-.281-1.438-.502-1.684-.494-2.405.058-1.618 "
             "1.239-3.869 1.355-5.894.299z",
                ['m', [246.026, 120.178], 'c', [-0.558, -0.295, -1.186, -0.768, -1.395, -1.054],
                 'c', [-0.314, -0.438, -0.132, -0.456, 1.163, -0.104],
                 'c', [2.318, 0.629, 3.814, 0.383, 5.298, -0.873],
                 'l', [1.308, -1.103], 'l', [1.54, 0.784],
                 'c', [0.848, 0.428, 1.748, 0.725, 2.008, 0.656],
                 'c', [0.667, -0.176, 2.05, -1.95, 2.005, -2.564],
                 'c', [-0.054, -0.759, 0.587, -0.568, 0.896, 0.264],
                 'c', [0.615, 1.631, -0.281, 3.502, -1.865, 3.918],
                 'c', [-0.773, 0.201, -1.488, 0.127, -2.659, -0.281],
                 'c', [-1.438, -0.502, -1.684, -0.494, -2.405, 0.058],
                 'c', [-1.618, 1.239, -3.869, 1.355, -5.894, 0.299],
                 'z', []
                ]),
        )
        failed = _testit(svglib.normalise_svg_path, mapping)
        self.assertEqual(len(failed) ,0)

    def test_relative_move_after_closepath(self):
        """
        A relative subpath is relative to the point *after* the previous
        closePath op (which is not recorded in path.points).
        """
        converter = svglib.Svg2RlgShapeConverter(None)
        node = svglib.NodeTracker(parseXML(
            '<path d="M0 0,0 1,1 1z m-1-1 0 1 1 0z"/>'
        ))
        # last point of this path should be 0 0
        path = converter.convertPath(node).contents[0]
        self.assertEqual(path.points[-2:] ,[0, 0])

    def test_cubic_bezier_shorthand(self):
        # If there is no previous command or if the previous command was not
        # an C, c, S or s, assume the first control point is coincident with
        # the current point.
        converter = svglib.Svg2RlgShapeConverter(None)
        node = svglib.NodeTracker(parseXML(
            '<path d="M3,4c-0.5-0.25-1.3-0.77-1.3-1.8h2.5s0.04,0.46,0.04,1.48z"/>'
        ))
        path = converter.convertPath(node).contents[0]
        self.assertEqual(path.operators ,[_MOVETO, _CURVETO, _LINETO, _CURVETO, _CLOSEPATH])
        self.assertEqual(path.points,
                [3.0, 4.0, 2.5, 3.75, 1.7, 3.23, 1.7, 2.2, 4.2, 2.2, 4.2, 2.2, 4.24, 2.66,
                4.24, 3.68,]
                )

    def test_unclosed_paths(self):
        converter = svglib.Svg2RlgShapeConverter(None)
        node = svglib.NodeTracker(parseXML('<path d="M0,0 4.5,3 0,6M4.5,3H9" id="W"/>'))
        group = converter.convertPath(node)
        self.assertEqual(len(group.contents) ,1)
        #closed_path = group.contents[0]
        #unclosed_path = group.contents[1]
        #self.assertEqual(len(closed_path.points) ,len(unclosed_path.points))
        #self.assertEqual(closed_path.operators ,[)
        #   _MOVETO, _LINETO, _LINETO, _CLOSEPATH, _MOVETO, _LINETO, _CLOSEPATH]
        #self.assertEqual(unclosed_path.operators ,[)
        #   _MOVETO, _LINETO, _LINETO, _MOVETO, _LINETO]

    def test_empty_path(self):
        converter = svglib.Svg2RlgShapeConverter(None)
        node = svglib.NodeTracker(parseXML('<path id="W"/>'))
        group = converter.convertPath(node)
        self.assertEqual(group, None)

def force_cmyk(rgb):
    c, m, y, k = colors.rgb2cmyk(rgb.red,rgb.green,rgb.blue)
    return colors.CMYKColor(c,m,y,k,alpha=rgb.alpha)

class TestColorAttrConverter(SVGBasicTest):
    "Testing color attribute conversion."

    def test_0(self):
        "Test color attribute conversion."

        mapping = (
            ("red", colors.red),
            ("#ff0000", colors.red),
            ("#f00", colors.red),
            ("rgb(100%,0%,0%)", colors.red),
            ("rgb(255, 0, 0)", colors.red),
        )
        ac = svglib.Svg2RlgAttributeConverter()
        failed = _testit(ac.convertColor, mapping)
        self.assertEqual(len(failed) ,0)

    def test_1(self):
        "Test color attribute conversion to CMYK"

        mapping = (
            ("red", force_cmyk(colors.red)),
            ("#ff0000", force_cmyk(colors.red)),
            ("#f00", force_cmyk(colors.red)),
            ("rgb(100%,0%,0%)", force_cmyk(colors.red)),
            ("rgb(255, 0, 0)", force_cmyk(colors.red)),
            ("rgb(0,255, 0)", force_cmyk(colors.Color(0,1,0))),
            ("rgb(0, 0, 255)", force_cmyk(colors.Color(0,0,1))),
        )
        ac = svglib.Svg2RlgAttributeConverter(color_converter=force_cmyk)
        failed = _testit(ac.convertColor, mapping)
        self.assertEqual(len(failed) ,0)


class TestLengthAttrConverter(SVGBasicTest):
    "Testing length attribute conversion."

    def test_0(self):
        "Test length attribute conversion."

        mapping = (
            ("0", 0),
            ("316", 316),
            ("-316", -316),
            ("-3.16", -3.16),
            ("-1e-2", -0.01),
            ("1e-5", 1e-5),
            ("1e1cm", 10*cm),
            ("1e1in", 10*inch),
            ("1e1%", 10),
            ("-8e-2cm", (-8e-2)*cm),
            ("20pt", 20 * 1.25),
        )
        ac = svglib.Svg2RlgAttributeConverter()
        failed = _testit(ac.convertLength, mapping)
        self.assertEqual(len(failed) ,0)


    def test_1(self):
        "Test length attribute conversion."

        ac = svglib.Svg2RlgAttributeConverter()
        attr = "1e1%"
        expected = 1
        obj = ac.convertLength(attr, 10)
        self.assertEqual(obj ,expected)


class TestLengthListAttrConverter(SVGBasicTest):
    "Testing length attribute conversion."

    def test_0(self):
        "Test length list attribute conversion."

        mapping = (
            (" 5cm 5in", [5*cm, 5*inch]),
            (" 5, 5", [5, 5]),
        )
        ac = svglib.Svg2RlgAttributeConverter()
        failed = _testit(ac.convertLengthList, mapping)
        self.assertEqual(len(failed) ,0)


class TestTransformAttrConverter(SVGBasicTest):
    "Testing transform attribute conversion."

    def test_0(self):
        "Test transform attribute conversion."

        mapping = (
            ("",
                []),
            ("scale(2) translate(10,-20.5)",
                [("scale", 2.0), ("translate", (10.0, -20.5))]),
            ("scale(0.9), translate(27,40)",
                [("scale", 0.9), ("translate", (27.0, 40.0))]),
            # Invalid expression returns empty list
            ("scale(0.9), translate",
                []),
        )
        ac = svglib.Svg2RlgAttributeConverter()
        failed = _testit(ac.convertTransform, mapping)
        self.assertEqual(len(failed) ,0)


class TestAttrConverter(SVGBasicTest):
    "Testing multi-attribute conversion."

    def test_0(self):
        "Test multi-attribute conversion."

        mapping = (
            ("fill: black; stroke: yellow",
                {"fill":"black", "stroke":"yellow"}),
        )
        ac = svglib.Svg2RlgAttributeConverter()
        failed = _testit(ac.parseMultiAttributes, mapping)
        self.assertEqual(len(failed) ,0)

    def test_findAttr(self):
        """
        Whitespace in attribute values shouldn't disturb parsing.
        """
        ac = svglib.Svg2RlgAttributeConverter()
        node = parseXML('<rect fill=" #00A1DE\n"/>')
        self.assertEqual(ac.findAttr(node, 'fill') ,"#00A1DE")

    def test_no_fill_on_shape(self):
        """
        Any shape with no fill property should set black color in rlg syntax.
        """
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg xmlns="http://www.w3.org/2000/svg"
                 width="1200" height="800"
                 viewBox="0 0 36 24">
                <rect y="10" width="36" height="4"/>
            </svg>
        ''')))
        self.assertEqual(drawing.contents[0].contents[0].fillColor ,colors.black)

    def test_fillopacity(self):
        """
        The fill-opacity property set the alpha of the color.
        """
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg version="1.1"
                 xmlns="http://www.w3.org/2000/svg"
                 width="660" height="480">
                <polygon id="triangle" points="0,-29.14 -25.23, 14.57 25.23, 14.57"
                         stroke="#0038b8" stroke-width="5.5" fill-opacity="0"/>
            </svg>
        ''')))
        self.assertEqual(drawing.contents[0].contents[0].fillColor ,colors.Color(0, 0, 0, 0))

    def test_fillrule(self):
        converter = svglib.Svg2RlgShapeConverter(None)
        node = parseXML('<polygon fill-rule="evenodd"/>')
        poly = Polygon()
        converter.applyStyleOnShape(poly, node)
        self.assertEqual(poly.fillMode ,FILL_EVEN_ODD)

    def test_stroke(self):
        converter = svglib.Svg2RlgShapeConverter(None)
        node = parseXML('<path d="m0,6.5h27m0,5H0" stroke="#FFF"/>')
        path = Path()
        converter.applyStyleOnShape(path, node)
        self.assertEqual(path.strokeColor ,colors.white)
        self.assertEqual(path.strokeWidth ,1)


class TestApplyTransformOnGroup(SVGBasicTest):
    def test_translate_only_x(self):
        """
        When the second translate value is missing, 0 is assumed.
        """
        group = Group()
        converter = svglib.Svg2RlgShapeConverter(None)
        transform = "translate(10)"
        converter.applyTransformOnGroup(transform, group)
        self.assertEqual(group.transform ,(1, 0, 0, 1, 10, 0))


class TestTextNode(SVGBasicTest):
    def test_space_preservation(self):
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg width="777" height="267">
              <text style="fill:#000000; stroke:none; font-size:28;">
                <tspan>TITLE    1</tspan>
                <tspan x="-10.761" y="33.487">Subtitle</tspan>
              </text>
            </svg>
        ''')))
        main_group = drawing.contents[0]
        # By default, only two tspans produce String objects, the rest
        # (spaces/newlines) is ignored.
        self.assertEqual(len(main_group.contents[0].contents) ,2)
        self.assertEqual(main_group.contents[0].contents[0].text ,"TITLE 1")

        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg width="777" height="267" xml:space="preserve">
              <text style="fill:#000000; stroke:none; font-size:28;">%s<tspan>TITLE    1</tspan><tspan x="-10.761" y="33.487">Subtitle</tspan></text>
            </svg>
        ''' % (5*' '))))
        main_group = drawing.contents[0]
        self.assertEqual(main_group.contents[0].contents[0].text ,5*' ')
        self.assertEqual(main_group.contents[0].contents[1].text ,"TITLE    1")

        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg width="777" height="267">
              <text style="fill:#000000; stroke:none; font-size:28;">
                <tspan>TITLE    1</tspan>
                <tspan x="-10.761" y="33.487">Subtitle</tspan>
              </text>
              <text xml:space="preserve">  with   spaces </text>
            </svg>
        ''')))
        main_group = drawing.contents[0]
        # xml:space can be overriden per text node
        self.assertEqual(main_group.contents[0].contents[0].text ,"TITLE 1")
        self.assertEqual(main_group.contents[1].contents[0].text ,'  with   spaces ')

    def test_tspan_position(self):
        """
        The x/y positions of a tspan are either relative to the current text
        position, or can be absoluted by specifying the x/y attributes.
        """
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg width="777" height="267">
              <text x="10" style="fill:#000000; stroke:none; font-size:28;">
                <tspan>TITLE 1</tspan>
                <!-- position relative to current text position-->
                <tspan>(after title)</tspan>
                <!-- absolute position from parent start -->
                <tspan x="-10.75" y="33.487">Subtitle</tspan>
              </text>
            </svg>
        ''')))
        main_group = drawing.contents[0]
        self.assertEqual(main_group.contents[0].contents[0].x ,10)
        self.assertEqual(main_group.contents[0].contents[0].y ,0)
        self.assertTrue(main_group.contents[0].contents[1].x > 10)
        self.assertEqual(main_group.contents[0].contents[1].y ,0)
        self.assertEqual(main_group.contents[0].contents[2].x ,-0.75)
        self.assertEqual(main_group.contents[0].contents[2].y ,-33.487)


class TestPolylineNode(SVGBasicTest):
    def test_filling(self):
        converter = svglib.Svg2RlgShapeConverter(None)
        node = svglib.NodeTracker(parseXML(
            '<polyline fill="none" stroke="#000000" '
            'points="10,50,35,150,60,50,85,150,110,50,135,150" />'
        ))
        polyline = converter.convertPolyline(node)
        self.assertIsInstance(polyline, PolyLine)

        # svglib simulates polyline filling by a fake polygon.
        node = svglib.NodeTracker(parseXML(
            '<polyline fill="#fff" stroke="#000000" '
            'points="10,50,35,150,60,50,85,150,110,50,135,150" />'
        ))
        group = converter.convertPolyline(node)
        self.assertIsInstance(group.contents[0], Polygon)
        self.assertEqual(group.contents[0].fillColor ,colors.white)
        self.assertIsInstance(group.contents[1], PolyLine)


class TestUseNode(SVGBasicTest):
    def test_use(self):
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg version="1.1"
                 xmlns="http://www.w3.org/2000/svg"
                 xmlns:xlink="http://www.w3.org/1999/xlink"
                 width="10cm" height="3cm" viewBox="0 0 100 30">
              <defs>
                  <rect id="MyRect" width="60" height="10"/>
              </defs>
              <rect x=".1" y=".1" width="99.8" height="29.8"
                    fill="none" stroke="blue" stroke-width=".2" />
              <use x="20" y="10" xlink:href="#MyRect" />
              <use x="30" y="20" xlink:href="#MyRect" fill="#f00" />
            </svg>
        ''')))
        main_group = drawing.contents[0]
        # First Rect
        self.assertIsInstance(main_group.contents[0], Rect)
        # Second Rect defined by the use node (inside a Group)
        self.assertIsInstance(main_group.contents[1].contents[0], Rect)
        self.assertEqual(main_group.contents[1].contents[0].fillColor ,colors.black) #default
        # Attributes on the use node are applied to the referenced content
        self.assertIsInstance(main_group.contents[2].contents[0], Rect)
        self.assertEqual(main_group.contents[2].contents[0].fillColor ,colors.red)

    def test_transform_inherited_by_use(self):
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg version="1.1"
                 xmlns:xlink="http://www.w3.org/1999/xlink"
                 width="900" height="600">
                <g id="c">
                    <path id="t" d="M 0,-100 V 0 H 50"
                          transform="rotate(18 0,-100)"/>
                    <use xlink:href="#t" transform="scale(-1,1)"/>
                </g>
            </svg>
        ''')))
        cgroup_node = drawing.contents[0].contents[0]
        self.assertEqual(
            cgroup_node.contents[0].transform,cgroup_node.contents[1].contents[0].transform
            )

    def test_use_forward_reference(self):
        """
        Sometimes, a node definition pointed to by xlink:href can appear after
        it has been referenced. But the order should remain.
        """
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg version="1.1"
                 xmlns:xlink="http://www.w3.org/1999/xlink"
                 width="900" height="600">
                <use xlink:href="#back" x="-100"/>
                <rect id="back" x="42" y="42" width="416" height="216" fill="#007a5e"/>
            </svg>
        ''')))
        self.assertEqual(len(drawing.contents[0].contents) ,2)
        self.assertIsInstance(drawing.contents[0].contents[0], Group)
        self.assertIsInstance(drawing.contents[0].contents[1], Rect)

    def test_use_node_properties(self):
        """
        Properties on the use node apply to the referenced item.
        """
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg version="1.1"
                 xmlns:xlink="http://www.w3.org/1999/xlink"
                 width="900" height="600">
                <path id="a" fill="#FF0000" d="M-15 37.57h60L-15 0v80h60l-60-60z"/>
                <use stroke="#003893" stroke-width="5" xlink:href="#a"/>
                <use stroke="#003893" stroke-width="2" xlink:href="#a"/>
            </svg>
        ''')))
        use_path1 = drawing.contents[0].contents[1].contents[0].contents[0]
        use_path2 = drawing.contents[0].contents[2].contents[0].contents[0]
        # Attribute from <path> node
        self.assertEqual(use_path1.fillColor ,colors.Color(1, 0, 0, 1))
        # Attribute from <use> node
        self.assertEqual(use_path1.strokeWidth ,5)
        self.assertEqual(use_path2.strokeWidth ,2)

    if 0:
        def test_use_node_with_unclosed_path(self):
            """
            When a <use> node references an unclosed path (which is a group with two
            different paths for filling and stroking), the use properties shouldn't
            affect the no-stroke property of the fake stroke-only path.
            """
            drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
                <?xml version="1.0"?>
                <svg xmlns="http://www.w3.org/2000/svg"
                     xmlns:xlink="http://www.w3.org/1999/xlink"
                     width="900" height="600" viewBox="0 0 9 6">
                    <defs>
                        <path d="M0,0 4.5,3 0,6" id="X"/>
                    </defs>
                    <use xlink:href="#X" fill="#f00" stroke="#ffb612" stroke-width="2"/>
                </svg>
            ''')))
            use_group = drawing.contents[0].contents[0].contents[0]
            self.assertEqual(use_group.contents[0].getProperties()['strokeWidth'] ,0)
            self.assertEqual(use_group.contents[0].getProperties()['strokeColor'], None)


class TestViewBox(SVGBasicTest):
    def test_nonzero_origin(self):
        drawing = svglib.svg2rlg(io.StringIO(textwrap.dedent(u'''\
            <?xml version="1.0"?>
            <svg xmlns="http://www.w3.org/2000/svg"
                 width="1200" height="800"
                 viewBox="-60 -40 120 80">
                <g fill="#E70013">
                    <rect x="-60" y="-40" width="120" height="80"/>
                </g>
            </svg>
        ''')))
        # Main group coordinates are translated to match the viewBox origin
        self.assertEqual(drawing.contents[0].transform ,(1, 0, 0, -1, 60.0, 40.0))

def makeSuite():
    return makeSuiteForClasses( TestPaths, TestColorAttrConverter, TestLengthAttrConverter, TestLengthListAttrConverter,
                                    TestTransformAttrConverter, TestAttrConverter, TestApplyTransformOnGroup,
                                    TestTextNode, TestPolylineNode, TestUseNode, TestViewBox,
                )

if __name__ == "__main__":
    import sys
    w = os.path.dirname(sys.argv[0])
    if w: os.chdir(w)
    unittest.TextTestRunner().run(makeSuite())
