/ jmablog / blog /

TinieR 0.1.0

August 17, 2020

TLDR: I wrote an R package. You can find it here: tinieR - An R package to shrink image filesizes with TinyPNG.com

I learnt R about a year ago to do the statistical testing for my dissertation. While borne of neccessity, it ended up kickstarting a whole coding journey for me. I’d done some dabbling in the past, but R was the first language that I felt like I really “got” and worked in a way that made sense to me, particularly with the Tidyverse series of packages.

I’ve since experimented with Python and JavaScript (particularly with SquatCam), but R will always hold a special place in my heart and be my first choice for a coding project, if possible. Looking for ways to keep progressing my skills, package development kept cropping up, and so I felt like it would be a really good way to get even more familiar with the language. The only trouble was, I didn’t really have any good ideas for things that a) I felt warranted a package and b) were within the scope of my current knowledge.

TinyPNG is a service I’ve been using for ages. It lets you upload PNG or JPG files and compresses them, resulting in much smaller file sizes, without any noticeable loss in image quality. I’ve used it for pretty much every image on this blog. In particular, all the graphs I like endlessly populating my posts with make excellent candidates for TinyPNG to shrink.

After using TinyPNG for so long via their web interface, I finally decided to investigate a more economical way of using the service. They have a whole bunch of third-party plugins and tools listed on their site that people have built. While investigating and trying out a Node.js CLI, I realised that I knew just enough about making REST calls to APIs in R that I could potentially just code myself an integration that could run directly in my RMarkdown files for blog posts!

I threw together a minimum working version of a function named tinify() that took a path to an image file and sent it off to the TinyPNG API, saving the result. It worked great - well enough that it occurred to me that other people might like to use it as well…

A few bumps along the way

Turns out package development, even for the simplest of packages with just two actual functions, is complicated.

First, as an invaluable resource, I used the R Packages book by Hadley Wickham and Jennifer Bryan extensively. It’s a great beginning-to-end guide to building a package. In particular, all the various things you need - licenses, readmes, namespaces, test, docs - are really easy to populate your package with. You still need to fill them in, but as a result, you’re far less likely to end up missing something that should be there.

But, for some things, a guide can only really hold your hand so far. For example, it can tell you the mechanics of how to create a test file, what it should consist of, and how to run it, but it can’t write the actual test for you, more’s the pity.

Testing in particular is something I’d never done before. The testthat package is an amazing tool for this, and I was able to get the basics down fairly quickly. In the end, I probably went overboard on my tests - I only had the two functions, so I had the luxury of not needing to write hundreds of tests for dozens of different things. Consequently, I really tried to cover all the bases for the two lonely functions I did have. I’m proud to have reached 98% code coverage as a result though!

When I was fairly happy with the state of my package, my thoughts turned to deployment. This blog post by Matt Dray on using GitHub Actions was really useful in pointing me in the right directions to set up a continuous deployment workflow and creating an automatically generated package documentation site. While again, probably overkill for this particular package, I wanted to really get to grips with the full package development experience.

One particular hurdle I encountered was the need for a valid TinyPNG API key and an example image file to be present for all my behind-the-scenes package code - namely tests and R CMD checks run on deployment by GitHub Actions. I didn’t want to expose my personal TinyPNG API token to the world, so needed a way to include it without making it visible to the public. It took a little experimenting but you can set your API key as an encrypted secret on your GitHub repo, and then access it by placing it as an environment variable (env) in the workflow yaml file using ${{ secrets.SECRET_NAME }}:

jobs:
    steps:
        name: Check
            env:
            _R_CHECK_CRAN_INCOMING_REMOTE_: false
            TINY_API: ${{ secrets.TINY_API_KEY }}
            run: rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check")
            shell: Rscript {0}

This environment variable is then accessed within my package code as part of the tinify() function with Sys.getenv("TINY_API").

As for an example image, this turned out to require putting my example.png file in a sub-directory of my package named inst/extdata. This file could then be accessed using system.file("extdata", "example.png", package = "tinieR"). As I didn’t want to be continually overwriting this file again and again every time I tested the package, I ended up learning about tempfile() and unlink() as a way to create a temporary file path, copy the example.png image to it, submit the temporary file to my function for testing, and then delete the temporary file.

As an aside, this worked fine for clearing all my tests, but I kept getting errors when R CMD check would try and run any of this in the ‘examples’ section of my documentation. I have to admit I never cracked this one and just ended up wrapping all my example code in my docs in \dontrun{} - if anyone knows how I should be doing this, please let me know 😕

After all this fiddling, getting my first ‘build passing’ badge show up on my GitHub repo readme was one of the most satisfying feelings I’ve had in coding so far!

tinieR

So, here it is - tinieR on GitHub or as a pkgdown package documentation site. With it, you can shrink image file sizes with a simple call of tinify():

tinify("example.png")

You can set options to overwrite the original file or create a new file next to it, to provide details on the amount of file size reduction and the number of API calls made this month, and to return the tinified image file path for use in other image functions. You can also combine tinify with functions like purrr::map and fs::dir_ls to tinify entire directories at a time. Check out the links above for full details!

If you find any bugs, please let me know. This is my first package and I’m determined to keep learning as I try and make it the best it can be!

Thanks for reading. I’ve been messing around with coding projects for a little while now - see more of them by clicking here, or for posts using R specifically, here.

Previous Next

📎 Tags:

coding r development tinier

📂 Recent posts in Blog: