63

I'm working on a kind of unique app which needs to generate images at specific resolutions according to the device they are displayed on. So the output is different on a regular Windows browser (96ppi), iPhone (163ppi), Android G1 (180ppi), and other devices. I'm wondering if there's a way to detect this automatically.

My initial research seems to say no. The only suggestion I've seen is to make an element whose width is specified as "1in" in CSS, then check its offsetWidth (see also How to access screen display’s DPI settings via javascript?). Makes sense, but iPhone is lying to me with that technique, saying it's 96ppi.

Another approach might be to get the dimensions of the display in inches and then divide by the width in pixels, but I'm not sure how to do that either.

Community
  • 1
  • 1
  • possible duplicate of [How to detect page zoom level in all modern browsers?](http://stackoverflow.com/questions/1713771/how-to-detect-page-zoom-level-in-all-modern-browsers) – Madara's Ghost Aug 04 '12 at 08:26

15 Answers15

19

<div id='testdiv' style='height: 1in; left: -100%; position: absolute; top: -100%; width: 1in;'></div>
<script type='text/javascript'>
  var devicePixelRatio = window.devicePixelRatio || 1;
  dpi_x = document.getElementById('testdiv').offsetWidth * devicePixelRatio;
  dpi_y = document.getElementById('testdiv').offsetHeight * devicePixelRatio;
  
  console.log(dpi_x, dpi_y);
</script>

grabbed from here http://www.infobyip.com/detectmonitordpi.php. Works on mobile devices! (android 4.2.2 tested)

Stanislav Kvitash
  • 4,614
  • 18
  • 29
MaxXx1313
  • 610
  • 5
  • 15
  • 4
    All you are doing is multiplying the constant 96 by the devicePixelRatio (which can be overridden). This is not a consistent way of getting the accurate ppi. – GreySage Jun 22 '17 at 19:18
  • I can't figure out why you think so. – MaxXx1313 Jun 26 '17 at 12:48
  • 12
    because the words that I typed have meanings? On all devices, 1in in css is defined to be 96px, so measuring a div with a width=1in in px will always give 96. THen you are just multiplying that (which again, will always be 96) by the devicePixelRatio which is not subject to standards or limitations, it could be anything and in particular anyone could overwrite it with whatever value they want. In short, this method isn't reliable. – GreySage Jun 26 '17 at 14:34
  • Actually, it doesn't work in firefox. Firefox itself looks weird on 4k display. In chrome and ie it forks perfectly fine. – MaxXx1313 Jun 27 '17 at 13:38
  • 1
    i'm afraid not... The assumption in the code is that "in" represent a real, physical inch in the screen. It is not. Sadly. – Avi Tshuva Jun 21 '22 at 13:55
13

I came up with a way that doesn't require the DOM... at all

The DOM can be messy, requiring you to append stuff to the body without knowing what stuff is going on with width: x !important in your stylesheet. You would also have to wait for the DOM to be ready to use...

/**
 * Binary search for a max value without knowing the exact value, only that it can be under or over
 * It dose not test every number but instead looks for 1,2,4,8,16,32,64,128,96,95 to figure out that
 * you thought about #96 from 0-infinity
 *
 * @example findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches)
 * @author Jimmy Wärting
 * @see {@link https://stackoverflow.com/a/35941703/1008999}
 * @param {function} fn       The function to run the test on (should return truthy or falsy values)
 * @param {number}   start=1  Where to start looking from
 * @param {function} _        (private)
 * @returns {number}          Intenger
 */
function findFirstPositive (f,b=1,d=(e,g,c)=>g<e?-1:0<f(c=e+g>>>1)?c==e||0>=f(c-1)?c:d(e,c-1):d(c+1,g)) {
  for (;0>=f(b);b<<=1);return d(b>>>1,b)|0
}

var dpi = findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches)

console.log(dpi)
Endless
  • 34,080
  • 13
  • 108
  • 131
  • 5
    This code works pretty well indeed, but is not readable at all – jrmgx Sep 30 '16 at 15:40
  • Should the result be exact? My monitor documentation says 93 while your function says 96. – Supersharp Nov 15 '16 at 16:11
  • The number should be exact. It makes assumption by doing n^2 upon till the function returns false and then works closer towards the number by taking the middle number. You can try it here: [fiddle](https://jsfiddle.net/12t6gxzh/1). If the function says it's 93 then it should be 93. Think there is something fundamental broken with the browser in that way browser don't know what the screen resolution is so it just assumes it's 96 – Endless Nov 15 '16 at 20:31
  • 5
    This always returns 96, even on mobile devices. This falls victim to the classic blunder of not realizing that inch is defined at 96px. – GreySage Jun 22 '17 at 19:32
  • +1 for effort, but... both this code and the code linked by @eltomito claim my phone PPI is ~336 when it's actually 518. And they both incorrectly claim my Mac and PC (with have the same raw resolution but different screen sizes) both have a PPI of 96 when in fact the actual values are 102 and 157, respectively. – Michael Feb 08 '18 at 03:53
  • this will not work on WebKit/Safari (iOS 11), it appears max-resolution is not supported there yet – Eugen Feb 14 '18 at 15:00
  • Okay, so it seems the problem is that mobile browsers render sites into a hidden invisible viewport and then rescale them and display them on the actual screen. The DPI and dimensions the browser reveals to the javascript code and CSS are that of the viewport, not of the actual display (which IMHO means the DPI is totally useless). The dimensions of the viewport can be controlled by the tag. Read more about it on MDN: https://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_taghttps://developer.mozilla.org/en-US/docs/Mozilla/Mobile/Viewport_meta_tag – eltomito Mar 09 '18 at 14:04
  • I had a similar idea, where you just keep testing `@media screen and (resolution: 96dpi)` and increasing `dpi` until you get a match. But testing it quickly on Chrome reveals that unfortunately, these CSS `resolution` `@media` queries also seem to scale up/down when the user zooms in/out, just like `window.devicePixelRatio` does. So unfortunately they are also useless. – V. Rubinetti Sep 15 '20 at 18:14
  • 2
    Here is a nicer version: https://repl.it/@duzun/findDPIjs#script.js – DUzun Sep 30 '20 at 07:09
  • @DUzun i like the bit shifting part (left & right) instead of *2 and /2, looks like your dose a better job at finding #97, also more readability, local variables and no ternary operator. What's wrong with ternary operators? don't like them? :P – Endless Sep 30 '20 at 12:33
  • @Endless Shifting gets ride of the fractional part, which is relevant for DPI. I like the ternary operator, only in simple expressions. Here I've replaced it for better readability and to avoid one call to `matchMedia()`. – DUzun Oct 01 '20 at 14:47
  • @DUzun Thanks for reference to https://repl.it/@duzun/findDPIjs#script.js -- this works perfect on Chrome/FireFox/Edge/Opera on desktop PC. I need to correctly render images at native resolution for best quality. Dividing this by W3C 96 standard tells me resolution zoom factor. So whether OS DPI is 100% and Chrome Zoom 100%, versus OS DPI 200% and Chrome Zoom 50% (same thing), the image will render correct. Resolution-adaptive rendering (e.g. custom 8-bit-pixel-art style CANVAS graphics needs to automatically round off to nearest 1:1 or 2:2 or 3:3 blocky-pixel point sampling scaling). – Mark Rejhon Dec 07 '22 at 07:35
11

There is the resolution CSS media query — it allows you to limit CSS styles to specific resolutions:

However, it’s only supported by Firefox 3.5 and above, Opera 9 and above, and IE 9. Other browsers won’t apply your resolution-specific styles at all (although I haven’t checked non-desktop browsers).

Paul D. Waite
  • 96,640
  • 56
  • 199
  • 270
  • 2
    Here is a playground to test it http://home.heeere.com/tech-ppi-aware-css-media-query.html – Rémi Jan 18 '15 at 16:13
  • Wow, I can only imaging creating a style sheet listing every possible resolution in 1 or 0.1dpi range increments and inferring the closest actual screen resolution based on which style was applied... – Michael Feb 08 '18 at 03:26
  • This doesn't seem accurate at all... the answer by Endless appears to do a binary search with this query, yet across my devices the playground linked by @Rémi gives the same invalid results... – Michael Feb 08 '18 at 03:56
4

Here is what works for me (but didn't test it on mobile phones):

<body><div id="ppitest" style="width:1in;visible:hidden;padding:0px"></div></body>

Then I put in the .js: screenPPI = document.getElementById('ppitest').offsetWidth;

This got me 96, which corresponds to my system's ppi.

Paul D. Waite
  • 96,640
  • 56
  • 199
  • 270
john
  • 145
  • 1
  • 4
  • 21
    This doesn't work on all devices. My Android phone reports 96dpi too. – Pointy Jan 29 '12 at 16:07
  • 7
    This doesn't seem to work on any devices. Everything seems to report 96ppi: http://codepen.io/anon/full/mrfvg – mckamey Nov 16 '12 at 18:46
  • It SHOULD works, but unfortunately all browsers consider 1in = 96px and 1cm == 37.8px. Bug? – Yukulélé Apr 04 '14 at 11:02
  • 10
    Not a bug - "px" isn't screen pixels, it is a fixed length of 0.75 pt, which itself is 1/72th of an inch. – Mathew Mar 20 '15 at 02:33
  • 4
    the CSS spec defines 1 px as 1/96th of an inch, which is why your calculation will always return 96. That inch is not an inch on-screen, but takes the distance to your eye into account, meaning that it's different on different devices. The browser vendors need to set that factor to something meaningful, which is not always the case. – Andy May 21 '15 at 12:59
4

Reading through all these responses was quite frustrating, when the only correct answer is: No, it is not possible to detect the DPI from JavaScript/CSS. Often, the operating system itself does not even know the DPI of the connected screens (and reports it as 96 dpi, which I suspect might be the reason why many people seem to believe that their method of detecting DPI in JavaScript is accurate). Also, when multiple screens are connected to a device forming a unified display, the viewport and even a single DOM element can span multiple screens with different DPIs, which would make these calculations quite challenging.

Most of the methods described in the other answers will almost always result in an output of 96 dpi, even though most screens nowadays have a higher DPI. For example, the screen of my ThinkPad T14 has 157 dpi, according to this calculator, but all the methods described here and my operating system tell me that it has 96 dpi.

Your idea of assigning a CSS width of 1in to a DOM element does not work. It seems that a CSS inch is defined as 96 CSS pixels. By my understanding, a CSS pixel is defined as a pixel multiplied by the devicePixelRatio, which traditionally is 1, but can be higher or lower depending on the zoom level configured in the graphical interface of the operating system and in the browser.

It seems that the approach of using resolution media queries produces at least some results on a few devices, but they are often still off by a factor of more than 2. Still, on most devices this approach also results in a value of 96 dpi.

cdauth
  • 6,171
  • 3
  • 41
  • 49
3

I also needed to display the same image at the same size at different screen dpi but only for Windows IE. I used:

<img src="image.jpg" style="
    height:expression(scale(438, 192)); 
    width:expression(scale(270, 192))" />

function scale(x, dpi) {

    // dpi is for orignal dimensions of the image
    return x * screen.deviceXDPI/dpi;
}

In this case the original image width/height are 270 and 438 and the image was developed on 192dpi screen. screen.deviceXDPI is not defined in Chrome and the scale function would need to be updated to support browsers other than IE

Farid Z
  • 960
  • 1
  • 9
  • 18
3

DPI is by definition tied to the physical size of the display. So you won't be able to have the real DPI without knowing exactly the hardware behind.

Modern OSes agreed on a common value in order to have compatible displays: 96 dpi. That's a shame but that's a fact.

You will have to rely on sniffing in order to be able to guess the real screen size needed to compute the resolution (DPI = PixelSize / ScreenSize).

Vincent Robert
  • 35,564
  • 14
  • 82
  • 119
  • 7
    DPI by definition is **not** the `numberOfPixels / sizeOfMonitorInInches`. DPI is defined such that 10 point text appears on your screen to be the same size as standard 10 point text in a book. In print, and typography, `1 inch = 72 points`. The critical distinction is that people usually have their monitor's further away than they hold a book (33% farther, in fact). That is where Microsoft got the value value `96 dpi` from: `72 * 33% = 96`. Ideally you would have your monitor as far away from your face as you typically hold a book. But you don't, so that is why you have 96dpi. – Ian Boyd Jun 21 '13 at 20:56
  • 4
    That comment is full of errors. DPI is an abbreviation that means dots per inch. With a screen, you are usually provided with the size of the diagonal in inches. To calculate how many pixels lie on this diagonal, you must work out the square root of (x^2 + y^2) where x and y are the horizontal and vertical pixels. For example, for 1920x1080 on a 24" display, the DPI is approx 91.8. And 72*33% is approx 24; you mean 72*133%. Probably 96DPI was no more than the mean scale of monitors at the time, and has nothing to do with the distance you hold a book compared to that of a monitor. – rbncrthms May 23 '14 at 11:29
3

The reply from @Endless is pretty good, but not readable at all, this is a similar approche with fixed min/max (it should be good ones)

var dpi = (function () {
    for (var i = 56; i < 2000; i++) {
        if (matchMedia("(max-resolution: " + i + "dpi)").matches === true) {
            return i;
        }
    }
    return i;
})();

matchMedia is now well supported and should give good result, see http://caniuse.com/#feat=matchmedia

Be careful the browser won't give you the exact screen dpi but only an approximation

jrmgx
  • 719
  • 8
  • 24
  • 4
    This is definitely more readable. But the different in mine and yours are that mine only executed matchMedia 11 times to figure out that it's 96 where as your code tries every possible number between 56 and 2000 – Endless Nov 15 '16 at 20:06
  • this will not work on WebKit/Safari (iOS 11), it appears max-resolution is not supported there yet – Eugen Feb 14 '18 at 15:00
2
function getPPI(){
  // create an empty element
  var div = document.createElement("div");
  // give it an absolute size of one inch
  div.style.width="1in";
  // append it to the body
  var body = document.getElementsByTagName("body")[0];
  body.appendChild(div);
  // read the computed width
  var ppi = document.defaultView.getComputedStyle(div, null).getPropertyValue('width');
  // remove it again
  body.removeChild(div);
  // and return the value
  return parseFloat(ppi);
} 

(From VodaFone)

Neuron
  • 5,141
  • 5
  • 38
  • 59
TJS101
  • 490
  • 1
  • 7
  • 19
  • 3
    Doesn't work, gives 96 on both my 1080p android smartphone as well as my laptop. – nmz787 Jun 26 '14 at 07:34
  • your site doesn't show a properly sized 1 inch square here, on either of my 2 displays I have connected (refreshed the page after moving from one screen to the other) – nmz787 Sep 09 '22 at 19:35
1

I think your best approach is to combine the suggestion of the "sniffer" image with a matrix of known DPIs for devices (via user agent and other methods). It won't be exact and will be a pain to maintain, but without knowing more about the app you're trying to make that's the best suggestion I can offer.

FriendOfFuture
  • 2,608
  • 1
  • 21
  • 21
-1

I just found this link: http://dpi.lv/. Basically it is a webtool to discover the client device resolution, dpi, and screen size.

I visited on my computer and mobile phone and it provides the correct resolution and DPI for me. There is a github repo for it, so you can see how it works.

Adam
  • 191
  • 1
  • 2
  • 14
  • 2
    No they cannot tell physical device diagonal and always show up 13.3" on all of my devices. You can calculate the "pixel per inch" by it but truly it just cannot detect the device size. – Takol Jan 02 '15 at 07:19
  • @Takol It’s just the default value. You are meant to change it. Diagonals cannot be detected by JavaScript. See https://github.com/LeaVerou/dpi/issues/36 – Frederik Krautwald May 25 '15 at 20:24
  • @FrederikKrautwald And yet the question is "Detecting the system DPI/PPI from JS/CSS?" which this is not an answer to. – Michael Feb 08 '18 at 04:01
  • @Michael What? I was replying to Takoi in using Lea Verou's tool as linked to in the answer. I didn't provide an answer to the SO question. – Frederik Krautwald Feb 21 '18 at 12:40
  • This tool doesn't work. Open it in Chrome, zoom the page in, refresh, and you get a new value. – V. Rubinetti Sep 15 '20 at 18:17
-1

Can't you do anything else? For instance, if you are generating an image to be recognized by a camera (i.e. you run your program, swipe your cellphone across a camera, magic happens), can't you use something size-independent?

If this is an application to be deployed in controlled environments, can you provide a calibration utility? (you could make something simple like print business cards with a small ruler in it, use it during the calibration process).

alex
  • 5,213
  • 1
  • 24
  • 33
-1

Generate a list of known DPI: https://stackoverflow.com/a/6793227

Detect the exact device. Using something like:

navigator.userAgent.toLowerCase();

For example, when detecting mobile:

window.isMobile=/iphone|ipod|ipad|android|blackberry|opera mini|opera mobi|skyfire|maemo|windows phone|palm|iemobile|symbian|symbianos|fennec/i.test(navigator.userAgent.toLowerCase());

And profit!

haelmic
  • 541
  • 4
  • 18
  • 1
    This doesn't make any sense, the user agent doesn't contain any information about what screen I have attached to my device. – cdauth May 24 '22 at 10:48
-1

Readable code from @Endless reply:

const dpi = (function () {
    let i = 1;
    while ( !hasMatch(i) ) i *= 2;

    function getValue(start, end) {
        if (start > end) return -1;
        let average = (start + end) / 2;
        if ( hasMatch(average) ) {
            if ( start == average || !hasMatch(average - 1) ) {
                return average;
            } else {
                return getValue(start, average - 1);
            }
        } else {
            return getValue(average + 1, end);
        }
    }

    function hasMatch(x) {
        return matchMedia(`(max-resolution: ${x}dpi)`).matches;
    }

    return getValue(i / 2, i) | 0;
})();
-1

Maybe I'm a little bit steering off this topic...
I was working on a html canvas project, which was intended to provide a drawing canvas for people to draw lines on. I wanted to set canvas's size to 198x280mm which is fit for A4 printing. So I started to search for a resolution to convert 'mm' to 'px' and to display the canvas suitably on both PC and mobile.
I tried solution from @Endless ,code as:

const canvas = document.getElementById("canvas");
function findFirstPositive(b, a, i, c) {
  c=(d,e)=>e>=d?(a=d+(e-d)/2,0<b(a)&&(a==d||0>=b(a-1))?a:0>=b(a)?c(a+1,e):c(d,a-1)):-1
  for (i = 1; 0 >= b(i);) i *= 2
  return c(i / 2, i)|0
}
const dpi = findFirstPositive(x => matchMedia(`(max-resolution: ${x}dpi)`).matches)
let w = 198 * dpi / 25.4;
let h = 280 * dpi / 25.4;
canvas.width = w;
canvas.height = h;

It worked well on PC browser, showing dpi=96 and size was 748x1058 px;work well on PC
However turned to mobile devices, it was much larger than I expected: size: 1902x2689 px.can't work on mobile
After searching for keywords like devicePixelRatio, I suddenly realize that, I don't actually need to show real A4 size on mobile screen (under which situation it's actually hard to use), I just need the canvas's size fit for printing, so I simply set the size to:

let [w,h] = [748,1058];
canvas.width = w;
canvas.height = h;

...and it is well printed:well printed

rookie
  • 1
  • 1