Friday, August 30, 2024

Scalable Vector Graphics (svg) - Decomposing and Scaling Elements for Blogger

Last post I lamented the failure of any the svg graphs I had for DAG Hamilton workflows to render in Blogger. I set out to rectify this and have had some initial success.

My first attempt to get svg to show up in Blogger has the DAG Hamilton logo as its subject. My approach was to bring the svg elements down to the most basic primitives I could manage, then scale and translate their individual coordinates to bring them into the view.

Fortunately, I was able to find an example from someone who had successfully rendered svg in Blogger. The subject blog post is eleven years old. It appears svg never totally caught on for some platforms. Nonetheless, I used this as a template, and, after some initial success, managed to render the Hamilton logo.

The post will step through the individual path components of the Hamilton logo (7) and show the code used to transform the coordinates to make the svg elements render at an appropriate location and size. The individual path elements that make up the logo are a large number of 3 coordinate bezier curves. The manner in which the curves listed is very format specific to the platform that created them. Unfortunately, I cannot recall which online png to svg converter I used to create the svg. Portable Inkscape kept crashing on the large png file, so I brute forced the issue by using an online converter.

The first element of the logo is basically a purple background for the whole logo. A gap or divit can be seen just left of center on the upper part. I'll cover the manual "fixing" of this further on.



Second path (orange)



Third path (deep purple)





Fourth path (yellow top point)

                                        

Fifth path (light orangey left leg were the star to face out from the screen)

                                      

Sixth path (medium purple little triangle in the center)

                                      

Seventh path (light orangey little triangle to the right of center)

                                   


Fixing that niggling divit on the top half of the logo (the thin subvertical line).


                                     


Final product. This is the one svg inline drawing I was able to show (content size limitations of Blogger?)


The full logo refuses to render (although I swear it did before). Well, at least there is an svg element showing up on the page (a purple star). <sigh> Another png . . .

                                   


The code. I'm not strong on HTML, but it was necessary to edit this post inline. As part of that exercise, I included the post outline generation as part of the script.



# python 3.12


# blog_post_make_outline.py


"""

Attempt to scale, translate, and inline svg

elements for display in Blogger.

"""


import re


import pprint


import sys


import copy


import xml.etree.ElementTree as ET


# REGEX patterns.


PATHPAT = r'[<]path[ ]d[=]["]'


MPAT = r'M([-]*[0-9]+[.]*[0-9]*)[ ]([-]*[0-9]+[.]*[0-9]*)[ ]'


          # first bezier curve coord

BEZPAT = (r'C([-]*[0-9]+[.]*[0-9]*)[ ]([-]*[0-9]+[.]*[0-9]*)[ ]'

          # second bezier curve coord

          r'([-]*[0-9]+[.]*[0-9]*)[ ]([-]*[0-9]+[.]*[0-9]*)[ ]'

          # third bezier curve coord

          r'([-]*[0-9]+[.]*[0-9]*)[ ]([-]*[0-9]+[.]*[0-9]*)[ ]')


ZPAT = r'Z[ ]'


FILLPAT = (r'["] fill[=]["]([#][A-F0-9][A-F0-9][A-F0-9]'

           r'[A-F0-9][A-F0-9][A-F0-9])["][ ]')


#                 transform="translate(4756.96875,109.12890625)" 

#                 transform="translate(5619,4112)"/>

TRANSFORMPAT = (r'transform[=]["]translate[(]'

                r'([-]*[0-9]+[.]*[0-9]*)[,]([-]*[0-9]+[.]*[0-9]*)[)]["]')


# Output formats/constants.


BEZFMT = ('C{0:.7f} {1:.7f} '

          '{2:.7f} {3:.7f} '

          '{4:.7f} {5:.7f} ')


PATHFMT_OPEN = '<path d="'


PATHFMT_1 = 'M{mstartx:.5f} {mstarty:.5f} {path:s} Z '


PATHFMT_2 = '" fill="{fill:s}" transform="translate({translatex:.7f},{translatey:.7f})"'


PATHFMT_CLOSE = ' />'


SVG_TAG_OPEN = ('<svg xmlns="http://www.w3.org/2000/svg" '

                'xmlns:xlink="http://www.w3.org/1999/xlink" '

                "width='500px' height='500px'>")


SVG_TAG_CLOSE = '</svg>'


def parse_path(pathstring):

    """

    Capture path elements in a dictionary.


    pathstring is the svg string for the path (one line).


    For a path comprised entirely of bezier curves

    in the format (all one line):


    <ns0:path d="M0 0 C2.54601018 1.57157072 5.09846344 3.13131386 7.65625 4.68359375 C39.179 . . .  0 0 Z M-690.96875 4007.87109375 C-707.702 . . .  Z " fill="#C3368C" transform="translate(4756.96875,109.12890625)" />


    Returns dictionary.

    """

    retval = {}

    patpath = re.compile(PATHPAT)

    match = patpath.match(pathstring)

    startindex = match.span()[1]

    mpat = re.compile(MPAT)

    # MPAT

    match = mpat.match(pathstring[startindex:])

    mpatgroups = match.groups()

    retval['mpatgroups'] = []

    retval['mpatgroups'].append(mpatgroups)

    startindex += match.span()[1]

    bezpat = re.compile(BEZPAT)

    zpat = re.compile(ZPAT)

    retval['paths'] = []

    while match:

        pathpoints = []

        # BEZPAT

        match = bezpat.match(pathstring[startindex:])

        while match:

            # Sentinel.

            if not match:

                continue

            pathpoints.append(match.groups())

            startindex += match.span()[1]

            match = bezpat.match(pathstring[startindex:])

        retval['paths'].append(pathpoints)

        # ZPAT

        match = zpat.match(pathstring[startindex:])

        startindex += match.span()[1]

        # Then look for MPAT

        # MPAT

        match = mpat.match(pathstring[startindex:])

        # If MPAT not there, work on color and transform.

        if not match:

            continue

        startindex += match.span()[1]

        retval['mpatgroups'].append(mpatgroups)

    fillpat = re.compile(FILLPAT)

    match = fillpat.match(pathstring[startindex:])

    startindex += match.span()[1]

    print('adding fill . . .')

    fill = match.groups()[0]

    retval['fill'] = fill

    transformpat = re.compile(TRANSFORMPAT)

    match = transformpat.match(pathstring[startindex:])

    transform = match.groups()

    print('adding transform . . .')

    retval['transform'] = transform

    return retval


def parse_all_paths(svgfilepath):

    """

    Finds and parses all svg paths

    within an svg file (very format

    specific - bezier curves only).


    Returns list of dictionaries, one

    for each path line of the svg file.

    """

    # Do all paths in Hamilton logo.

    # Make list of dictionaries.

    patpath = re.compile(PATHPAT)

    with open(svgfilepath, 'r') as f:

        paths = []

        # Line one.

        next(f)

        # Line two.

        next(f)

        for linex in f:

            print(PATHPAT)

            print(linex[:30])

            match = patpath.match(linex)

            if not match:

                break

            paths.append(parse_path(linex))

    return paths


def work_paths(paths, fcn):

    """

    Apply an operation to all coordinates in

    the bezier curve paths represented in

    paths.


    Also covers translate and fill.


    paths is a list of dictionaries. Each

    dictionary represents one line of an

    svg file with a path made up of 

    bezier curves.

    """

    # Return value.

    newpaths = []

    # M - start of path segment.

    for pthx in paths:

        newmpatgroups = []

        for coordsx in pthx['mpatgroups']:

            newmpatgroups.append([fcn(x) for x in coordsx])

        newpaths.append({'mpatgroups':newmpatgroups})

    # to Z - end of path segment.

    # List of path dictionaries (paths).

    for pthx, newpath in zip(paths, newpaths):

        newpath['paths'] = []

        # Each M to Z path segment.

        for path in pthx['paths']:

            curvegroup = []

            for curve in path:

                newcurve = [fcn(x) for x in curve]

                curvegroup.append(newcurve)

            newpath['paths'].append(curvegroup)

    # transform and fill.

    for pthx, newpath in zip(paths, newpaths):

        newpath['transform'] = [fcn(x) for x in pthx['transform']]

        newpath['fill'] = pthx['fill']

    return newpaths


def translate_paths(paths, translation):

    """

    From a two tuple of x, y translation,

    adjust dictionary values for x, y 

    translation in each path in path

    list accordingly.


    Returns new dictionary

    """

    # TRANSLATE

    # ['mpatgroups', 'paths', 'fill', 'translate']

    translated_paths = copy.deepcopy(paths)

    for pathx in translated_paths:

        pathx['transform'][0] += translation[0]

        pathx['transform'][1] += translation[1]

    return translated_paths


def get_path_strings(paths):

    """

    From a list of path dictionaries, 

    builds one line strings for insertion

    into svg file.


    Returns list

    """

    pathdict_2 = {'fill':None,

                'translatex':None,

                'translatey':None}

    path_segment_dict = {'mstartx':None,

                         'mstarty':None,

                         'path':None}

    pathstrings = []

    for pathx in paths:

        # Copy and initialize fill/translate dictionary.

        fill_translate = copy.deepcopy(pathdict_2)

        fill_translate['fill'] = pathx['fill']

        fill_translate['translatex'] = pathx['transform'][0]

        fill_translate['translatey'] = pathx['transform'][1]

        # Zip together M and path segments.

        path_segs = zip(pathx['mpatgroups'], pathx['paths'])

        # For each path segment.

        path_strings = []

        for M, path_seg in path_segs:

            seg_dict = copy.deepcopy(path_segment_dict)

            seg_dict['mstartx'] = M[0]

            seg_dict['mstarty'] = M[1]

            # Build path segment string.

            path_seg_strings = [BEZFMT.format(*coords) for coords in path_seg]

            path = ''.join(path_seg_strings)

            seg_dict['path'] = path

            # Make final segment string with M (PATHFMT_1)

            path_with_M = PATHFMT_1.format(**seg_dict)

            path_strings.append(path_with_M)

        path_all_together = ''.join(path_strings)

        # Tack on fill/translate at end and beginning with d path flag.

        # Add to pathstrings.

        pathstrings.append(PATHFMT_OPEN  +

                           path_all_together +

                           PATHFMT_2.format(**fill_translate) +

                           PATHFMT_CLOSE)

    return pathstrings  


paths = parse_all_paths('hamilton_logo_large.svg')

print('len(paths) = {0:d}'.format(len(paths)))

pprint.pprint([x for x in paths[0]])


paths = work_paths(paths, float)


scale = 0.035

scaleit = lambda x: scale * x


paths = work_paths(paths, scaleit)


# pprint.pprint(paths[0]['paths'][0])


paths = translate_paths(paths, (125, 0))


pathstrings = get_path_strings(paths)


# with open('test_paths.txt', 'w') as f:

#     for pathx in pathstrings:

#         print(pathx, file=f)

#         print('\n\n', file=f)


GAP_FIX = "<polygon points='265 90.25, 245 143.75, 268 144.1, 295 90.25' style='fill: black;' />"

GAP_FIX_PROPER_COLOR = "<polygon points='265 90.25, 245 143.75, 268 144.1, 295 90.25' style='fill: #C3368C;' />"


TEXT = '<p>{0:s}</p>'


CODE = """

<p>&nbsp;</p><pre style="background: rgb(238, 238, 238); border-bottom-color: initial; border-bottom-style: initial; border-image: initial; border-left-color: initial; border-left-style: initial; border-radius: 10px; border-right-color: initial; border-right-style: initial; border-top-color: rgb(221, 221, 221); border-top-style: solid; border-width: 5px 0px 0px; color: #444444; font-family: &quot;Courier New&quot;, Courier, monospace; font-stretch: inherit; font-variant-east-asian: inherit; font-variant-numeric: inherit; line-height: inherit; margin-bottom: 1.5em; margin-top: 0px; overflow-wrap: normal; overflow: auto; padding: 12px; vertical-align: baseline;"><span style="font-size: 13px;">{0:s}</span></pre>

"""


textlist = ['blah blah blah',

            'blah blah blah again',

            'blah blah blah a third time',

            'blah blah blah a fourth time',

            'blah blah blah a fifth time',

            'blah blah blah a sixth time',

            'blah blah blah a seventh time',

            'fix divit',

            'final product']


fixed_divit = copy.deepcopy(pathstrings)

fixed_divit.insert(1, GAP_FIX_PROPER_COLOR)


svglist = [pathstrings[0:1],

           pathstrings[0:2],

           pathstrings[0:3],

           pathstrings[0:4],

           pathstrings[0:5],

           pathstrings[0:6],

           pathstrings[0:],

           pathstrings[0:] + [GAP_FIX],

           fixed_divit]


with open('blogpost.html', 'w') as f:

    for blah, svgels in zip(textlist, svglist):

        print(TEXT.format(blah), file=f)

        print(SVG_TAG_OPEN, file=f)

        for svgelement in svgels:

            print(svgelement, file=f)

        print(SVG_TAG_CLOSE, file=f) 


    print(TEXT.format('More blah about code'), file=f)


    print(CODE.format('>>> import this'), file=f)


print('Done')

Notes:

1) I had had good intentions of including a code box (the CODE string constant in the Python code) and I hit a bit of a wall. Not only is my code a mixed bag (this blog was always intended as a learning experience and a place for trying things out), it doesn't look good. We deal.

Which brings me to the point: you hear titles like front end developer, designer, website marketer etc. and think, "Well, it's kind of like art, kind of like coding . . . sort of creative." I now know, it's coding and it's thinking and it's grinding. All respect.

2) Steven Lott recently published a book that I bought (pdf). I have found it helpful. It's very pragmatic. I'm only about 15% of the way into it, but his treatment of regular expressions as just another tool not be scared of made me less tentative in my REGEX use (even if my REGEXes are far from elegant). Group capture with parens was really helpful for this exercise.

3) Our chief geology database administrator commented that the colors of the DAG Hamilton logo are those of the Spanish shawl nudibranch. There is a resemblance.

Photo courtesy of iNaturalist.



Thanks for stopping by.

No comments:

Post a Comment