Introduction to the problem
Introduction to the problem
The cover GIF is a rasterized version of Graphics with a bunch of usual primitives. They are designed to be exactly 1 pixel width and is intended to align perfectly with your monitor’s pixels at a specific ImageSize (here the first and last frame of the GIF). One frame of the graphics is included below, can you (especially for readers on Windows) find the correct ImageSize value for Show, so the grids align with pixels on your own screen?
Show
,ImageSize->{width,height}
Pixel based screen is one of the most common media when working with vector graphics. Pixel-perfection is essential when doing digital design, UI composition, etc. However, the computer world (or for sake of this post, the modern Windows desktop world) has a lot of units to measure length, some are related to physical units, many aren’t. These units have confused people (including me) for a long time. A recent question on the sister site, Why AbsoluteThickness depends on ImageSize?, motivated me to finally investigate the topic and get the concepts straight for good and for all. Now that I believe I have a correct understanding of the problem, I think it’s really worth sharing my findings as a tech note. Hopefully it can help other confused fellows to find their own ways to work with vector graphics in a pixel perfect way.
Helper functions
Helper functions
ClearAll[pipe,pipeList,branch,branchSeq]pipe=RightComposition;branch=Through@*{##}&;branchSeq=pipe[branch@##,Apply@Sequence]&;pipeList=pipe[Fold[branchSeq[Identity,pipe[#2,#1]]&,Identity,Reverse@{##}],List]&;
myRasterize is a modified version of Rasterize, so it works correctly for small raster size:
In[]:=
ClearAll[myRasterize]Options[myRasterize]=BackgroundRGBColor,,;myRasterize[graph_Graphics,RasterSizew_Integer,OptionsPattern[]]:=Block[{System`ConvertersDump`createExportPacketExpr,bg=OptionValue[Background]},System`ConvertersDump`createExportPacketExpr[expr_,opts___]:=System`ConvertersDump`createGraphicExportPacketExpr[expr,opts]//Block[{GraphicsBox=Inactive[GraphicsBox]},#/.(BackgroundNone):>Sequence[]]&//Activate;Rasterize[graph,RasterSizew,Backgroundbg]//pipe[RemoveAlphaChannel,ImageData[#,"Byte"]&,DeleteCases[{ImageData[Image[{{bg}}],"Byte",InterleavingTrue][[1,1]]..}],Image[#,"Byte",ColorSpace"RGB"]&]]
93
255
107
255
255
255
Usage:
Graphics[{},BackgroundBlack,ImageSize{10,5}]//pipe[branch[Rasterize[#,RasterSize10]&,myRasterize[#,RasterSize10]&],branch[Identity,Map@ImageDimensions],Prepend[{"Rasterize","myRasterize"}],Transpose,Grid]
Out[]=
Rasterize | {10,16} | |
myRasterize | {10,5} |
All the units in the world
All the units in the world
The inch is not an inch!
The inch is not an inch!
When talking about length on screen, people use inch a lot. Then there are concepts like DPI or dot per inch, PPI or pixel per inch, etc. When I measure my monitor, it’s width with in horizontal. It seems, by “definition” its PPI is
34.4cm
1920px
In[]:=
Quantity[1920,"Pixels"]
UnitConvert[Quantity[34.4,"Centimeters"],"Inches"]
Out[]=
Right? Now, according to the documentation of ImageSize, Wolfram Language uses print’s point for it and assumes per inch. My notebook Magnification is , so ImageSize 72 should give me a graphic exactly wide (i.e. ), right?
72pt
100
%
1inch
2.54cm
In[]:=
CurrentValue[Magnification]Graphics{},BackgroundBlack,AspectRatio,ImageSize72
1
10
Out[]=
1.
Out[]=
Apparently I’m wrong! It’s around :
2.15cm
What is happening? I failed to see a connection between and . That was the first roadblock on my quest.
2.54cm
2.15cm
After some searching and reading, it turned out (see MSDN reference at the end of the post) that on modern Windows desktop, the inch in DPI/PPI is called logical inch, which has nothing to do with the actual inch, and usually does not equate to an actual inch either.
Firstly, the printer’s point (or pt for short), more officially known as desktop publishing point, is an abstract length unit:
In[]:=
Quantity[1,"PrintersPoints"]//{#,#//QuantityUnit}&
Out[]=
,DesktopPublishingPoints
Windows introduced the concept of logical inch (), as I understand it, for compatibility, so this always holds:
in
logic
72pt196px
in
logic
Quote MSDN: “For many years, Windows used the following conversion: One logical inch equals 96 pixels.”
The dot is actually a pixel!
The dot is actually a pixel!
Having understood the logical inch, I can confidently say: Hey! My monitor is working at ! But what about DPI? If the inch is to logical inch, the point is a logical point, what is the dot in DPI?
96PPI
I went out and did some more research. A lot of materials came up, many of them emphasized the difference between DPI and PPI. Unfortunately, that was one of the source of my original confusion, because they are talking about other DPI meanings! Here, for Windows desktop, the dot in DPI IS the same as pixel! Quoting MSDN:
This scaling factor is described as 96 dots per inch (DPI). The term dots derives from printing, where physical dots of ink are put onto paper. For computer displays, it would be more accurate to say 96 pixels per logical inch, but the term DPI has stuck.
But the pixel is not a pixel!
But the pixel is not a pixel!
Okay... but wait! If , why is this Graphics wide instead of ?
72pt96px
120px
96px
In[]:=
Graphics{},BackgroundBlack,AspectRatio,ImageSize72
1
10
Out[]=
In[]:=
(*FromWindows'PrintScreen:*)
//ImageDimensions
Out[]=
{120,12}
The story only got stranger as it so happens that Windows Direct2D introduced yet another abstract/logical layer. The pixel or dot in relation , is something called device-independent pixel or DIP for short. It relates with the actual pixel size ( for short) on physical monitors through Windows DPI scaling factor , which is on my main display:
196px
in
logic
px
λ
DPI
1.25
Simply put, Windows aligns with on my monitor. Indeed, ! The same information can also be confirmed from SystemInformation:
100DIP
125px
96DIP×1.25px/DIP120px
In[]:=
SystemInformation["Devices","ConnectedDisplays"]//Lookup["Resolution"]//First
Out[]=
120.
Without diving deeper into the rabbit hole, it’s worth noting that there is still at least one extra layer between DIP and physical pixels: In cases the monitor is not working at native resolution (so the whole desktop and UI could look fuzzy), we’ll have to differentiate rendering pixels () from physical pixels (). For example:
px
rend
px
phy
A monitor with is used, but a non-native resolution is set. Then there will be area unused both at left and right, and a region at center will be re-mapped to for actual display. It means in that case , so physical pixel alignments can happen only at a few certain pixels.
1920×1080
px
phy
px
phy
1440×900
px
rend
px
rend
96×1080
px
phy
px
phy
1728×1080
px
phy
px
phy
1440×900
px
rend
px
rend
56
px
rend
px
phy
In this post, I shall assume native resolution are used, so ≡. Thus without differentiating them, is used afterwards for simplicity.
px
phy
px
rend
px
Wrap-up
Wrap-up
In summary, I now have encountered the following units and concepts: (Really a lot for a seemly simple problem!)
in | Ordinaryrealworldinch |
in logic | Logicalinch,noconnectionwithordinaryin |
pt | Desktoppublishingpoint |
dot | Device-independentpixel,alsoshortformDIPs |
DPI | dotsper in logic |
λ DPI | WindowsDPIscalingsetting |
px rend | “Renderingpixel”asreferredinOS’screenresolutionsettings |
px phy | Physicalpixelasonphysicaldisplays |
They relate to each other by:
72pt196dot≡96DIPs96
in
logic
λ
DPI
px
rend
Again, it is good to confirm my findings through SystemInformation:
Drawing Graphics pixel-perfectly
Drawing Graphics pixel-perfectly
Pixel alignment and anti-aliasing
Pixel alignment and anti-aliasing
Success! It looks like a pixel-perfect alignment is achieved (note 244 in first row is due to anti-aliasing):
By varying the horizontal position (i.e. xpos) of the testing line, I was able to identify the linear relationship between pixel coordinate and Graphics coordinate:
Moreover, the test clearly indicates a simple linear anti-aliasing stage. That means pixel value is proportional with fraction of the pixel area covered by graphic primitive.
I did a quick check to make sure that is consistent with what I saw directly from my monitor:
Note Round[255*{1/4, 3/4}] is exactly {64,191}:
Margins -- a hidden pitfall
Margins -- a hidden pitfall
Careful readers may already noticed my explicit settings on CellMargins and CellFrameMargins in the previous example. It so happens that these two options do affect the final result -- considering that they are both measured in pt, this shouldn’t come out as surprising!
Generally speaking, to place a Graphics, it must be contained in a Cell, which itself is in a Notebook. So parameters determining the Graphics’ position include: the top-left corner of the notebook window, the left and top margins from the Cell to notebook’s boundary, and the left and top margins from the Graphics to Cell’s boundary. They can be specified by WindowMargins, CellMargins and CellFrameMargins respectively. On Windows, a native window’s position is always pixel-aligned, so CellMargins and CellFrameMargins are the only settings I need to worry about. To have a pixel-aligned Graphics, it must first be contained in a cell that has its two margins add up to integral pixels:
A zoom-in of the top-left corner of my screen shows how everything is aligned perfectly:
Further zoom-in of the magenta region:
Neat examples
Neat examples
Case 1
Case 1
A series of interleaving vertical lines of 1px width:
zoom-in:
They all align with pixels exactly, so no antialias effect is triggered.
Case 2
Case 2
Some key features from my little artwork:
◼
The four corner pixels were left untouched.
◼
The left circle touched the rounded rectangle precisely. (Especially comparing with scaled up vector Graphics.)
For comparison, this shows a ×6 scale-up of the vector Graphics itself:
References
References