I was struggling for a while to find a ready-made formula to calculate Moon orientation at my location. Eventually, after several bits and pieces from the internet, I now have a formula for it (for my Northern Hemisphere, not sure whether it works for Southern Hemisphere).
I've written a more detailed blog describing 3 solutions you can get a Moon phase image file with the right moon orientation/tilt as seen from your location/sky here Moon Phase orientation as seen from your location/sky
The key value you need to calculate is the Moon Zenith Angle ((Moon Phase Angle (or called Position Angle of the Moon's Bright Limb in Jean Meeus's book) minus Moon Parallactic Angle)). You also need a have a correct/current Moon Phase with North-up orientation (vertical Moon Phase with Rabbit ears at 90 degree clockwise from North Starting Moon Position) to start with (I can get the current Moon Phase png file from CumulusMX software, it always produces the current Moon Phase with North-up orientation. I can also DIY the current Moon Phase, though a bit complicated. Let me know if you want to know how.).
From there, you may follow the orientation logic from my python code below based on the above assumption. I've verified its orientation with timeanddate moon phase website, Stellarium, and Skysafari.Click this link for the current oriented Moon at my Lat 14d N, 100.5E: current oriented Moon Phase
#def moonorientation():
# compute moon zenith angle, alitude, and azimuth at current time
# compute moon orientation as seen from my location by rotating moon.png...
# ... which is a moon phase image created by CumulusMX, and then save the rotated moon phase...
# ... in /var/www/html/moonphase_CumulusMX_rotated.png
from skyfield.api import N, E, load, wgs84
from skyfield.trigonometry import position_angle_of
from skyfield.framelib import ecliptic_frame
import datetime
# get current time in utc
now = datetime.datetime.utcnow()
ts = load.timescale()
t = ts.utc(now.year, now.month, now.day, now.hour, now.minute, now.second)
eph = load('de421.bsp')
sun, moon, earth = eph['sun'], eph['moon'], eph['earth']
# find Moon phase (0-180 = new to waxing to full moon, >180-0 is waning moon)
e = earth.at(t)
_, slon, _ = e.observe(sun).apparent().frame_latlon(ecliptic_frame)
_, mlon, _ = e.observe(moon).apparent().frame_latlon(ecliptic_frame)
phase = (mlon.degrees - slon.degrees) % 360.0
print("moon phase (180deg=full)",phase)
# find moon zenith angle, need observer location
# set my location
myLat = 13.71732
myLng = 100.5907
europacafe = earth + wgs84.latlon(myLat * N, myLng * E)
b = europacafe.at(t)
m = b.observe(moon).apparent()
s = b.observe(sun).apparent()
#print(position_angle_of(m.altaz(), s.altaz()).degrees)
moonangle = position_angle_of(m.altaz(), s.altaz()).degrees
print("zenith moon angle: ", moonangle)
# compute moon phase bright limb degree to be oriented from what seen at north pole
if phase > 180: #waning moon
moonorient = moonangle - 90
else: #waxing moon
moonorient = moonangle + 90
# as rotate function in html is limited to +180 and -180, so need adjustment
if moonorient > 180:
moonorient = moonorient - 360
elif moonorient < -180:
moonorient = moonorient + 360
moonorient = moonorient * -1
print("moon phase rotation angle, from upright phase, at my location: ", moonorient)
# from the above angle degree, rotate clockwise if it is positive degree
#return moonorient
#print(m.altaz())
# rotate moon phase (py rotate +deg = anti-clockwise)
from PIL import Image
img = Image.open(r"/home/bthoven/CumulusMX/web/moonES.png")
#rotate_img= img.rotate(moonorient, resample=Image.BICUBIC, expand=True)
rotate_img= img.rotate(moonorient * -1, resample=Image.BICUBIC)
#rotate_img.show()
rotate_img.save('/var/www/html/moonphase_CumulusMX_rotated.png')