Running Python (that works independently) inside Dynamo?

I have this code working in Python 3.11.5 running in my Windows Python environment and it is fine. It adds text the the bottom of images, in this case, each images’ filename. I would like it to run inside Dynamo.

from PIL import Image, ImageDraw, ImageFont
import os
input_folder = "D:\Dynamo Working\QR"
output_folder = "D:\Dynamo Working\QR2"
font = ImageFont.truetype('arial.ttf', 24)
for filename in os.listdir(input_folder):
    if filename.endswith('.jpg') or filename.endswith('.png'):
        image_path = os.path.join(input_folder, filename)
        image = Image.open(image_path)
        if image.mode == 'RGBA':
            image = image.convert('RGB')
        draw = ImageDraw.Draw(image)  
        text = os.path.splitext(filename)[0]      
        # Hardcoded text size and position
        text_width, text_height = 250, 50
        text_x = (image.width - text_width) / 2 + 50
        text_y = image.height - text_height - 10
        draw.text((text_x, text_y), text, font=font, fill=(0,0,0))
        output_path = os.path.join(output_folder, filename)
        image.save(output_path)

In Dynamo, I tried adding:

import sys
sys.path.append(r'C:\Users\desig\AppData\Roaming\Python\Python311\site-packages')
from PIL import Image, ImageDraw, ImageFont

But get this error:

Warning: ImportError : cannot import name '_imaging' from 'PIL' (C:\Users\desig\AppData\Roaming\Python\Python311\site-packages\PIL\__init__.py) ['  File "<string>", line 3, in <module>\n', '  File "C:\\Users\\desig\\AppData\\Roaming\\Python\\Python311\\site-packages\\PIL\\Image.py", line 82, in <module>\n    from . import _imaging as core\n']

Using sys.version 3.8.3 and Revit 2022.1.
Ref: How to install Python modules in Dynamo Core Runtime 2.8.0?

PS: If I get it to run, I suppose it will only work on my machine, not on a client’s unless the client has a compatible Python install and I path it correctly, eh?

If it executes externally, and you can send the dataset or whatever inputs are needed for the external tool, and have Dynamo execute the Python externally.

Is that the best way for these kinds of tasks?
Isn’t it possible to load a module inside Dynamo Python? EG: from PIL import Image, ImageDraw, ImageFont
In my external Python, I had to do pip install pillow in Terminal.

I find it to be the most stable. Getting ‘downstream consumers’ to install Python packages is a pain. Add in that it doesn’t run in older/newer/other Python builds and you’re into a horrible game I like to call “which version of my tool needs to be put on that system based on their Revit build and the current moon phase, dont forget to carry the one and account for if mercury is in retrograde.”
(I may need a new name for that but the pain in typing it all out almost equates the pain of installers).

Alternatively you could try to import it locally, that is also problematic if your tool needs additional references (ie: NumPy) or has a complex install process (ie: PyWin32).

Mixing development environments can also be painful, but if your Python based tool has value stand alone (sounds like it could), you might as well give them that as a bonus, and trigger it via Dynamo before moving into the next project.

3 Likes

Looks like good advice, @jacob.small . Seems that more stability and portability in this case would be to convert the Python to C# and make a Zero-Touch node with its DLL.

I suppose Zero-Touch Case Study - Python Scripts in C# · GitBook is not an answer because we’d still need to import Python modules.

IronPython and PythonNet have access to the .Net framework (a huge .Net library) so here is an alternative you can use

import sys
import clr
import System
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

clr.AddReference('System.Drawing')
from System.Drawing import *
from System.Drawing.Drawing2D import *

myBitmapPath = IN[0]
out_folder = System.IO.Path.GetDirectoryName(myBitmapPath)
out_bmp_path = out_folder + "\\out_img.png"
text_to_draw = IN[1]

bmp = Bitmap(myBitmapPath)
rectf = RectangleF(bmp.Size.Width / 2, bmp.Size.Height / 2  - 180, 160,  30)
g = Graphics.FromImage(bmp)
g.SmoothingMode = SmoothingMode.AntiAlias;
g.InterpolationMode = InterpolationMode.HighQualityBicubic
g.PixelOffsetMode = PixelOffsetMode.HighQuality
g.DrawString(text_to_draw, Font("Tahoma", 20), Brushes.Black, rectf)
g.Flush()
bmp.Save(out_bmp_path)

OUT = bmp
3 Likes

You can add pip and external libraries to the Dynamo python interpreter using the script in my post here Openpyxl module

If that is not your preferred method -or- you are using a more recent version of python and associated modules you could run a script that calls another version of python and use argparse library to pass arguments

import subprocess

def runpy(pyexe, script):
    """
    Run different version of python inside Dynamo
    Script is python list or string of script (including path)
    with command line arguments. Beware of spaces in file paths
    """
    if pyexe:
        comm = [pyexe]
        if isinstance(script, str):
            comm += script.split()
        elif isinstance(script, list):
            comm += script
        return subprocess.run(comm, capture_output=True, shell=True)


pyexe = IN[0]
script = IN[1]
result = runpy(pyexe, script)
OUT = result.stdout.decode(), result.stderr.decode()

So IN[0] path to python like this C:/Users/<username>/AppData/Local/Programs/Python/Python311/python.exe

And a basic script example with IN[1] as "C:/temp/test_script.py a b c"

import argparse
import platform

print(platform.python_version())
parser = argparse.ArgumentParser()
parser.add_argument("echo", nargs="*")
args = parser.parse_args()
if args.echo:
    print(args.echo)
else:
    print("No arguments here")

Result
image

3 Likes

With your hint and more help from Claude 2, I got it to run inside Dynamo!
From this:


To this:

import clr
clr.AddReference('RevitNodes')
import System
clr.AddReference('System.Drawing')
from System.Drawing import Bitmap,Graphics,Font,Brushes,RectangleF
from System.Drawing.Drawing2D import SmoothingMode,InterpolationMode,PixelOffsetMode
logo=Bitmap("D:\\Dynamo Working\\BSE-Logo50.png")
input_folder="D:\\Dynamo Working\\QR"
input_len = len(input_folder) + 1
output_folder="D:\\Dynamo Working\\QR2"
font=Font("Tahoma",14)
names=[]
for filename in System.IO.Directory.GetFiles(input_folder):
  bmp=Bitmap(filename)
  g=Graphics.FromImage(bmp)
  g.SmoothingMode=SmoothingMode.AntiAlias
  g.InterpolationMode=InterpolationMode.HighQualityBicubic
  g.PixelOffsetMode=PixelOffsetMode.HighQuality
  g.DrawImage(logo,265,10)
  text = filename[input_len:-4]
  names.append(text)
  text_size = g.MeasureString(text, font)
  text_x = (580 - text_size.Width) / 2 
  text_y=510
  rectf = RectangleF(text_x, text_y, text_size.Width, text_size.Height)
  g.DrawString(text,font,Brushes.Black,rectf)
  g.Flush()
  out_filename=System.IO.Path.Combine(output_folder,System.IO.Path.GetFileName(filename))
  bmp.Save(out_filename)
OUT=names
4 Likes