I am using Python to build an image-rendering app that renders numpy arrays as images. I need a way to automate the calculation screen DPI, as I am generating images of the same size on different monitors (e.g., 2x2 inches). I am in Windows 10, Python 3.7.
I am not asking how to get the monitor's screen size -- like finding out if your monitor is 3000x2000 pixels (as was solved at: How do I get monitor resolution in Python?).
Rather, I want something that calculates DPI speficially, and takes into account any scale settings the user has applied that might change the DPI of their system. E.g., I have the scale set to 250%.
Following this answer at experts-exchange (https://www.experts-exchange.com/questions/21794611/Detecting-system-DPI-settings-in-Python.html), I have tried using this:
from ctypes import windll
def get_ppi():
LOGPIXELSX = 88
LOGPIXELSY = 90
user32 = windll.user32
user32.SetProcessDPIAware()
dc = user32.GetDC(0)
pix_per_inch = windll.gdi32.GetDeviceCaps(dc, LOGPIXELSX)
print("Horizontal DPI is", windll.gdi32.GetDeviceCaps(dc, LOGPIXELSX))
print("Vertical DPI is", windll.gdi32.GetDeviceCaps(dc, LOGPIXELSY))
user32.ReleaseDC(0, dc)
return pix_per_inch
It seems to be the right ballpark, but returns a number that is too low. It returns 240, while my actual DPI is closer to 285.
I would prefer to avoid using a GUI framework like Qt for this, as I want to minimize overhead, but if that's the only way, then I will! If I do have to use a framework, I'd prefer PyQt5.
People regularly discuss digital images in terms of DPI, which stands for Dots Per Inch. The DPI of a digital image is calculated by dividing the total number of dots wide by the total number of inches wide OR by calculating the total number of dots high by the total number of inches high.
I play with a resolution of 1920x1080 and I find 1000 dpi to be enough. The higher the dpi - the harder it is to be accurate - for me anyway.
Divide the pixel width that you wrote down by the width in images that you measured to get the image's horizontal DPI. For instance, if the pixel width is 1524 pixels and the width in inches is six, you have this equation: Horizontal_DPI = 1524 / 6.
Pixel density Now, the resolution is expressed in dpi (or ppi), which is the acronym for dots (or pixels) per inch. So, if you see 72 dpi it means that the image will have 72 pixels per inch; if you see 300 dpi means 300 pixels per inch, and so on.
While I said I wanted to avoid it, there is one very simple way to pull this off using PyQt5. The more I think about it, the more I think this could be the best solution, as it is largely platform independent:
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
screen = app.screens()[0]
dpi = screen.physicalDotsPerInch()
app.quit()
Note that app.screens()
returns a list of screens. In my case I only have one attached but you may have multiple, so be sure to be aware of which screen you need to get dpi from. And if you keep all this contained in a function, it won't clutter your namespace with PyQt junk.
Also, for more on QScreen (sc
is a QScreen object) see this doc page:
https://doc.qt.io/qt-5/qscreen.html
There's all sorts of cool stuff you can pull from it.
While @eric's answer works, I would prefer to avoid leaving the standard library (ignoring the distributions of Python that don't include ctypes
).
This initially led me to ctypes and you can see my initial (very over-complicated) answer which can be found below. More recently I found a way to do it with just Tk (I can't remember the link to it so will link to a Stack Overflow answer which has the same content)
import tkinter
root = tkinter.Tk()
dpi = root.winfo_fpixels('1i')
Original answer
The following answer offers two solutions:
The first is Windows' reported DPI due to the user's display scaling
The second is the monitor's true DPI calculated by finding the monitor's physical size and resolution
These solutions assume there is only one monitor and also sets process DPI awareness (which won't be suitable for some contexts). For details on the Windows API calls made by ctypes
see the documentation for SetProcessDPIAwareness
, GetDPIForWindow
, GetDC
and GetDeviceCaps
.
Solution 1
This solution doesn't return a proper DPI, and is instead just Window's "zoom factor" for that monitor. To get the factor, you should divide the returned DPI by 96
(meaning 96
is a scale factor of 100%
while 120
means a scale factor of 125%
, etc.).
The method uses tkinter
to get a valid HWND
(an identifier for the window) and then ctypes
to get the DPI for the new window.
# Import the libraries
import ctypes
import tkinter
# Set process DPI awareness
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# Create a tkinter window
root = tkinter.Tk()
# Get the reported DPI from the window's HWND
dpi = ctypes.windll.user32.GetDpiForWindow(root.winfo_id())
# Print the DPI
print(dpi)
# Destroy the window
root.destroy()
Solution 2
This method should return the monitor's true DPI, however, two things should be noted:
The physical size of the monitor is (occasionally) reported incorrectly. This will therefore result in a completely incorrect DPI value, though I am not sure how to prevent this except add a check to ensure it is between sensible values (whatever that means!).
The resolution Windows is using is often different to the monitor's actual resolution. This method calculates the monitor's true DPI, but if you want to calculate the number of virtual pixels per physical inch, you would replace the assignment of dw
and dh
with root.winfo_screenwidth()
and root.winfo_screenheight()
(respectively)
This method uses tkinter
to get a valid HWND
(an identifier for the window) and then ctypes
to get a Device Context (DC) & hardware details from this.
# Our convertion from millimeters to inches
MM_TO_IN = 0.0393700787
# Import the libraries
import ctypes
import math
import tkinter
# Set process DPI awareness
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# Create a tkinter window
root = tkinter.Tk()
# Get a DC from the window's HWND
dc = ctypes.windll.user32.GetDC(root.winfo_id())
# The the monitor phyical width
# (returned in millimeters then converted to inches)
mw = ctypes.windll.gdi32.GetDeviceCaps(dc, 4) * MM_TO_IN
# The the monitor phyical height
mh = ctypes.windll.gdi32.GetDeviceCaps(dc, 6) * MM_TO_IN
# Get the monitor horizontal resolution
dw = ctypes.windll.gdi32.GetDeviceCaps(dc, 8)
# Get the monitor vertical resolution
dh = ctypes.windll.gdi32.GetDeviceCaps(dc, 10)
# Destroy the window
root.destroy()
# Horizontal and vertical DPIs calculated
hdpi, vdpi = dw / mw, dh / mh
# Diagonal DPI calculated using Pythagoras
ddpi = math.hypot(dw, dh) / math.hypot(mw, mh)
# Print the DPIs
print(round(hdpi, 1), round(vdpi, 1), round(ddpi, 1))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With