Let's continue exploring the Haskell diagrams library. In this post, we will embed external images in our diagrams.
Motivation
I would like to create images from Haskell diagrams with embedded images. At the same time, I want to use diagrams' cairo backend (diagrams-cairo) instead of the svg
backend.
Program
I am going to need 3 Haskell packages for this Literate Haskell program: diagrams, diagrams-cairo and markdown-unlit. It is noted in the diagrams documentation page that the cairo
backend is a little tricky to install in some environments. Luckily, nixpkgs
has a well-packaged version of diagrams-cairo
:
{
##...
ghc = pkgs.haskellPackages.ghcWithPackages (hpkgs: [
## ...
hpkgs.diagrams
hpkgs.diagrams-cairo
hpkgs.markdown-unlit
## ...
]);
thisShell = pkgs.mkShell {
buildInputs = [
## ...
ghc
## ...
];
NIX_GHC = "${ghc}/bin/ghc";
NIX_GHCPKG = "${ghc}/bin/ghc-pkg";
NIX_GHC_DOCDIR = "${ghc}/share/doc/ghc/html";
NIX_GHC_LIBDIR = "${ghc}/lib/ghc-9.6.5/lib";
};
# ...
}
We will need to import a few modules.
import Diagrams.Backend.Cairo
import Diagrams.Prelude
import System.Environment (getArgs)
Unlike the previous post, we are importing Diagrams.Backend.Cairo
instead of Diagrams.Backend.SVG
. The API interface is pretty much the same, though.
Why cairo
? Firstly, image clipping does not work with the diagrams-svg
backend for some reason. I guess why, but I will not speculate here. Secondly, I want to see how diagrams-cairo
backend works for me.
We will generate a few diagrams in this tutorial. Let's implement our entry point:
main :: IO ()
main = do
dir <- head <$> getArgs
logo <- loadLogo
render dir "diagram-logo.png" logo
render dir "diagram-logo-clipped.png" (mkAvatar logo)
render dir "diagram-logo-clipped-with-text.png" (mkAvatarWithText "The Negation" logo)
render dir "diagram1.svg" (mkAvatarWithText "SVG (~20KB)" logo)
render dir "diagram1.png" (mkAvatarWithText "PNG (~12KB)" logo)
render dir "diagram1.jpg" (mkAvatarWithText "JPG (~12KB)" logo)
where
render dpath fname = renderCairo (dpath <> "/" <> fname) (mkSizeSpec2D (Just 240) Nothing) . frame 0.2
This is how we will run our blog post:
runhaskell \
-pgmLmarkdown-unlit \
content/posts/2024-08-10_haskell-diagrams-images.lhs \
static/assets/media/posts/haskell-diagrams-images
Our objective is to create an avatar image for my blog. It will be a circle with my Website's logo in it. We will also add some text under the avatar image.
This is how my Website's logo looks like as a raw PNG:
Let's load the image inside a Diagram B
value. We are scaling the diagram to 10
:
loadLogo :: IO (Diagram B)
loadLogo = do
(Right img) <- loadImageEmb "./static/android-chrome-512x512.png"
pure $ scaleUToX 10 $ image img
Let's create a function to clip a given image in a circle. We will use the clipBy
function to achieve this. Since our square image is scaled to 10
, We are using a circle with a radius of 5
:
mkAvatar :: Diagram B -> Diagram B
mkAvatar =
clipBy (circle 5)
I also want to add some text under the image:
mkAvatarWithText :: String -> Diagram B -> Diagram B
mkAvatarWithText txt img =
vsep 0.3 $
[ mkAvatar img
, text txt # fontSizeL 1 # fc black # frame 1
]
That's all! Let's see how SVG, PNG and JPG images look like:
Wrap-Up
I am getting used to the diagrams library. This onboarding process will take some time until I get the fundamentals right, but I am happy with the progress so far. It is also reassuring to see that this library is out for quite some time, it is actively maintained, and there is a decent community around it.
I will continue to explore the library in my future posts.