3

I was wondering if would be possible to load a svg format like shape and with loop to repeat with for loop to get some kind a generic pattern. Most what I have founded researching online, is to convert from svg to png or some else format but I was wondering is it possible to manipulate before converting to some format (jpg)?

I have tried to combine cairosvg and PIL but I have not gone too far.

from cairosvg import svg2png
from PIL import Image, ImageDraw

white = (255,255,255)
img = Image.new('RGB', (300,300), white)
draw = ImageDraw.Draw(img)

svg_code = """
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="12" r="10"/>
        <line x1="12" y1="8" x2="12" y2="12"/>
        <line x1="12" y1="16" x2="12" y2="16"/>
    </svg>
"""

Usually I would use something like this...

for x in range(0, 300, 25):
    draw.svg_code??

But this doesn't work... Any idea how can I load svg format and use it with other modules?

Thanks in advance!

Firefoxer
  • 265
  • 3
  • 15
  • Manipulate the SVG itself to do patterns. See [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs) and [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g). – Ouroborus Jul 17 '22 at 20:41
  • @Ouroborus I would prefer to do it the way I explained because I can integrate it in other solutions. – Firefoxer Jul 18 '22 at 18:56
  • 1
    Another option: Convert the svg to a transparent png. Create a new image and draw the png to it multiple times to create your pattern. Afaik, there's no module that can use a svg as a brush or stamp. – Ouroborus Jul 18 '22 at 19:20

2 Answers2

2

Requirements

  • load a shape in svg format
  • create a background pattern
  • manipulate before converting to jpg with PIL

Skia

Google Chrome, Android, Flutter and others use the open source 2D graphics library Skia, for which there is also a Python binding.

Skia allows to create a shader with tile mode rules to fill a drawn area. Additionally a matrix can be supplied which allows e.g. rotation. Advantage is that this is performed very efficiently even if the target image is large and the number of elements that make up the pattern is very high.

Such a pattern can therefore be easily created, e.g. with the following code:

def pattern(canvas, image_element, rotation):
    matrix = skia.Matrix()
    matrix.preRotate(rotation)
    canvas.drawPaint({
        'Shader': image_element.makeShader(
            skia.TileMode.kRepeat,
            skia.TileMode.kRepeat,
            matrix,
        )
    })

So a complete approach might look like this:

  • convert SVG string (or file) to Skia image with the appropiate size
  • create the pattern as background
  • manipulate it by drawing additional elements like rectangles/text
  • convert it to PIL image and save it

An important feature for many use cases is that SVG drawings are done on transparent background.

However, JPEG does not support transparency. You can convert the image from a type with transparency (RBGA) to a type without transparency by using .convert('RGB'). But in this case you will have black elements on a black background. When converting, it is possible to create a new image with the background preset and paste the transparent image into it.

Self-contained Python example

A self-contained example that takes into account the above points might look like the following program.

It simply creates a pattern based on your SVG example and draws a red text on a gray rectangle on it. It outputs two images: once the desired JPEG but also PNG to be able to display the mentioned transparency option.

import io
import skia
from PIL import Image


def image_from_svg(svg, element_size):
    stream = skia.MemoryStream()
    stream.setMemory(bytes(svg, 'UTF-8'))
    svg = skia.SVGDOM.MakeFromStream(stream)
    width, height = svg.containerSize()
    surface = skia.Surface(element_size, element_size)
    with surface as canvas:
        canvas.scale(element_size / width, element_size / height)
        svg.render(canvas)
    return surface.makeImageSnapshot()


def pattern_image_with_title(image_element, width, height, rotation, title):
    surface = skia.Surface(width, height)
    with surface as canvas:
        pattern(canvas, image_element, rotation)
        rectangle(canvas, width)
        draw_title(canvas, title)
    return surface.makeImageSnapshot()


def draw_title(canvas, title):
    paint = skia.Paint(AntiAlias=True, Color=skia.ColorRED)
    canvas.drawString(title, 48, 76, skia.Font(None, 32), paint)


def rectangle(canvas, width):
    rect = skia.Rect(32, 32, width - 32, 96)
    paint = skia.Paint(
        Color=skia.ColorGRAY,
        Style=skia.Paint.kFill_Style)
    canvas.drawRect(rect, paint)


def pattern(canvas, image_element, rotation):
    matrix = skia.Matrix()
    matrix.preRotate(rotation)
    canvas.drawPaint({
        'Shader': image_element.makeShader(
            skia.TileMode.kRepeat,
            skia.TileMode.kRepeat,
            matrix,
        )
    })


def write_png(file_name, skia_image):
    with io.BytesIO(skia_image.encodeToData()) as f:
        pil_image = Image.open(f)
        pil_image.save(file_name, 'PNG')


def write_jpeg(file_name, skia_image, background):
    with io.BytesIO(skia_image.encodeToData()) as f:
        pil_image = Image.open(f)
        new_image = Image.new("RGBA", pil_image.size, background)
        new_image.paste(pil_image, (0, 0), pil_image)
        new_image = new_image.convert('RGB')
        new_image.save(file_name, 'JPEG')


svg_code = """
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="12" r="10"/>
        <line x1="12" y1="8" x2="12" y2="12"/>
        <line x1="12" y1="16" x2="12" y2="16"/>
    </svg>
"""

if __name__ == "__main__":
    img = image_from_svg(svg_code, 50)
    img = pattern_image_with_title(img, 300, 300, 45, 'hello world!')
    write_png('result.png', img)
    write_jpeg('result.jpg', img, 'WHITE')

Result

As a result of the above program you get two images. On the left is the JPEG image with white background and on the right is the PNG image with transparency.

example result

Stephan Schlecht
  • 26,556
  • 1
  • 33
  • 47
  • I have one additional question, when I change svg code, when I use svg code from inkscape I get this error `width, height = svg.containerSize()` `AttributeError: 'NoneType' object has no attribute 'containerSize'`. Any idea how do I solve this? – Firefoxer Jul 30 '22 at 20:07
  • I don't know, I tried the following briefly: When I export a simple geometric shape as "Plain SVG", it works as expected. – Stephan Schlecht Aug 01 '22 at 07:38
1

I understand that the question is tagged python, but am providing an ImageMagick-based answer because:

  • you may be unaware that Python is not necessary here, and there are simpler solutions, and
  • you/we can readily port the code below to Python using wand which is a Ctypes binding to ImageMagick if you like the approach

Note that you can find questions and answers tagged as wand by putting [wand] into the StackOverflow search box if you want to see some examples.


So, if you save your SVG in a file called image.svg, you can run the following ImageMagick command in your Terminal (Linux/macOS) or Command Prompt (Windows):

magick -background none image.svg -write mpr:tile +delete \
       -size 400x200 tile:mpr:tile result.gif

enter image description here

Or:

magick -background yellow image.svg -write mpr:tile +delete \
       -size 400x200 tile:mpr:tile result.gif

enter image description here


Or, if you want a specific number of copies, say 21 "in toto" repeated 3 images per line:

magick -background magenta image.svg -duplicate 20 miff: | magick montage -tile 3x -geometry +0+0 miff:- result.gif

enter image description here


Or 20 "in toto", spaced a little apart, organised into 2 rows:

magick -background cyan image.svg -duplicate 19 miff: | magick montage -background cyan -tile x2 -geometry +5+5 miff:- result.gif

enter image description here


If you have an old v6 ImageMagick, in the above commands you need to:

  • replace magick montage with montage, and
  • replace plain magick with convert

Note that there is lots of interesting material on tiled/repeating backgrounds by Anthony Thyssen here.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432