Hip Barcodes

PA 5 — Due: Fri Nov 3 11pm CT

Recall the academic integrity policy: turn in your own work and cite all sources.

This week we are going to make our Code 128 barcodes more hip. First, we will support switching between code sets to optimize barcode lengths. Second, we will generate and interpret barcodes as images, rather than bitstrings, using the Haskell Image Processing (HIP) Library. Lastly, there is an optional challenge idea to scan barcode images designed with some extra-hip flair.

% cabal run hip-barcodes -- encode images/barcode.png 200 5 "Hip, hip, hooray\!"
...
Wrote to:
images/barcode.png
% cabal run hip-barcodes -- decode images/barcode.png
Decoding:
Hip, hip, hooray!

Hip, but uncool…

Note: HIP currently does not work with the latest version of Haskell. So, we will use a handy tool, called GHCup, that allows installing and running older versions of Haskell (see instructions below). ❗TRY IT OUT ASAP❗

Starter Code

The starter code contains several files:

Switching Between Code Sets

Last time, we supported barcodes encoded entirely with code set B or entirely with code set C. Now let’s support switching between code sets B and C, via the values “[Code B]” and “[Code C]”. As we will soon discuss, mixing and matching code sets allows shorter barcodes in some cases. (We are still ignoring code set A in this assignment, so “[Shift]” values will remain unsupported symbols.)

encode :: TheCodes -> String -> Either Error BC
decode :: TheCodes -> BC -> Either Error String

1. Decoding

Decoding is the easier part, so let’s start with that.

The decode function should be a relatively simple combination of decodeB and decodeC from before, plus new support for switching between code sets B and C. Consider defining a helper function for decoding that uses an argument — of type Boolean or some other two-valued type, such as data CodeSet = B | C — to track which code set is currently active.

As you rework decodeB and decodeC into one, look for opportunities to use fmap (a.k.a. (<$>)) on Either Error values. That is, wherever you might write something like

case SOME_RESULT of
  Left err -> Left err
  Right x  -> Right $ SOME_FUNCTION x

consider rewriting as:

SOME_FUNCTION <$> SOME_RESULT

2. Optimal Encoding (Extra Credit)

Optimizing barcode length during encoding is the harder part. This is optional and worth a small amount of extra credit.

Skip this part for now (the starter code sets up a strawman implementation of encode that makes a simple (non-optimal) choice between encodeB and encodeC) and come back after working on the image processing parts. If and when you are ready to work on encode, uncomment the following line in Tester.hsrunTests $ tests_encode theCodes.

Consecutive digits require one symbol using code set C instead of two symbols using B. But each switch costs a symbol.

When is it profitable to use code set C? The Wikipedia article provides a nice summary. (Below, “N+” means mean “N or more” as opposed to some kind of Haskell section!)

Hints

Turn the outline above into code. (No need for dynamic programming as suggested in the Wikipedia article, just fiddly list munging.) The first two cases — entire string and beginning of string — should be straightforward, if a bit tedious. The latter two cases — middle and end of string — are trickier.

Define simple helpers processB and processC for processing a (sub)string entirely with code set B or C, respectively.

Define a (much longer) helper which, assuming that code set B is active, takes an input string with a non-empty prefix of non-digit characters and (i) encodes that prefix with code set B and (ii) decides what to do with the rest of the string — with the help of all the helpers.

Check out these standard Prelude functions:

span :: (a -> Bool) -> [a] -> ([a], [a])

splitAt :: Int -> [a], ([a], [a])

even :: Integral a => a -> Bool

odd :: Integral a => a -> Bool
> span isDigit "01234abc56789de"
("01234","abc56789de")

> splitAt 1 [1..10]
([1],[2,3,4,5,6,7,8,9,10])

HIP Library

We will use the Haskell Image Processing (HIP) Library to manipulate barcode images.

The build-depends entry of our .cabal file includes hip, a package containing modules Graphics.Image, Graphics.Image.ColorSpace, Graphics.Image.Interface, and more.

If you try to build, you will get an error that looks something like:

% cabal build
...
[__3] skipping: base-4.18.0.0, base-4.17.1.0, base-4.17.0.0 (has the same
characteristics that caused the previous version to fail: excluded by
constraint '>=4.8 && <4.17' from 'repa')
...
[__3] fail (backjumping, conflict set: base, hip-barcodes, repa)
...

The trouble is hip depends on repa (>=3.2.1.1 && <4), which depends on base (>=4.8 && <4.17). But this is incompatible with the current stable versions of GHC: 9.4 corresponds to base 4.17, 9.6 to 4.18, and 9.8 to 4.19.

GHCup

There is a nice tool called GHCup to manage the installation of updates to GHC and associated tools.

It is fun to imagine GHCup organizing different versions of the Haskell toolchain into separate cups. Thanks, Sam, for this delightful double entendre!

See what versions of GHC and related tools are installed on your machine:

% ghcup list

To get HIP to work, install the following older version:

% ghcup install ghc 9.2.8

To set which version of GHC is active:

% ghcup set ghc 9.2.8
[ Info  ] GHC 9.2.8 successfully set as default version

Check that you are now running the older version:

%  ghci
GHCi, version 9.2.8: https://www.haskell.org/ghc/  :? for help
>

When you’re done working on this assignment, you can reset things to the most recent version. You can do that by running set without a version number:

% ghcup set ghc
[ Info  ] GHC 9.4.7 successfully set as default version

In addition to the commands above, you can also run the following (on Linux or Mac, but not Windows):

% ghcup tui

Check out this sweet terminal UI! Almost makes you want to say hello again to Hello, Again Wordle, doesn’t it?

Cabal

Try this right now to make sure your development environment works.

% cabal build

In addition to compiling, also check that you can access hip via GHCi for interactively exploring the library and testing your program. (Notice the handy cabal repl command, which will launch ghc and load the packages listed in the .cabal file.)

% cabal repl hip-barcodes
> import Graphics.Image

Two more options:

% ghci
> :set -package hip
> import Graphics.Image

Alternatively, you might run the following to install hip in a more global place so it’s available from GHCi without :set -package hip.

% cabal install --lib hip
% ghci
> import Graphics.Image

Overview

Part of the goal in this assignment is to practice working with a large library… even if we don’t grok all the types and type classes and type families (a Haskell feature we are not discussing in this course) and multi-parameter type classes (another such feature).

You will likely need only a handful of functions provided by the library. As a first example, the following loads an image and gets a pixel at a given position:

> :set -package hip
> import Graphics.Image
> img <- readImageRGB VU "images/barcode-1.png"
> let pixel = index img (0, 0)
> pixel
<RGB:(0.0|0.0|0.0)>
> let PixelRGB r g b = pixel in (r, g, b)
(0.0,0.0,0.0)
> toPixelY pixel
<Luma:(0.0)>
> let PixelY y = toPixelY pixel in y
0.0

This example demonstrates:

Now consider a second example, that creates an image:

> cabal repl hip-barcodes
> import Graphics.Image
> let grayscale :: Image VU Y Double = makeImageR VU (200, 200) (\(i, j) -> PixelY (fromIntegral i / 200))
> writeImage "images/grayscale.png" grayscale

This example demonstrates:

Not demonstrated above, but a few more possibly helpful functions, among others:

3. Making Barcodes

Now that you’re hip to HIP, it’s time to get barcoding!

First, a function for generating barcode images:

makeBarcode :: FilePath -> Int -> Int -> BCString -> IO ()
makeBarcode filePath imageHeight moduleWidth (BCString symbols) =
  ...

The imageHeight is specified in pixels, and moduleWidth argument is the number of pixels for each module. The image should not have quiet zones on the left or right, nor any padding on top or bottom.

4. Scanning Barcodes

Next, implement a barcode “scanner.” Given a “perfect” barcode image generated from makeBarcode (no rotation, no graininess, no defects whatsover), decode it:

scanBarcode :: FilePath -> IO BCString
scanBarcode filePath =
  ...

Consider implementing the following helper functions. (No need to worry about MArray — just note VU is a member of this class.)

imageToBCString :: (MArray arr cs e, ToY cs e) => Image arr cs e -> BCString

isBlack :: (ToY cs e) => Pixel cs e -> Bool

It is important to notice that scanBarcode does not know either the imageHeight or the moduleWidth that was used to create the image. Do not assume they are 200 and 5 as in the sample at the top of the page. Determining the height is not a big deal. Determining module width requires a bit of finagling. Consider implementing and using the following two helpers:

getModuleWidth :: [Bool] -> Int

takeEvery :: Int -> [a] -> [a]

Tests

The images/ directory in the starter code provides a handful of barcodes to test:

% cabal run hip-barcodes -- decode images/barcode-0.png | grep -A1 Decoding
Decoding:
Hip, hip, hooray!

% cabal run hip-barcodes -- decode images/barcode-1.png | grep -A1 Decoding
Decoding:
Haskell!

% cabal run hip-barcodes -- decode images/barcode-2.png | grep -A1 Decoding
Decoding:
CMSC 22300? HOT Programming in Haskell!

% cabal run hip-barcodes -- decode images/barcode-3.png | grep -A1 Decoding
Decoding:
3141592653

% cabal run hip-barcodes -- decode images/barcode-4.png | grep -A1 Decoding
Decoding:
098x1234567y23

These tests are also included in Tester.hs:

% cabal run tests

5. Scanning Designed Barcodes (Optional)

Wikipedia showcases some “designed barcodes”:

By Hans Haase
Own work, CC BY-SA 4.0, Link

By Pemba.mpimaji
Own work, CC BY-SA 4.0, Link

By Hans Haase
Own work, CC BY-SA 4.0, Link

By Hans Haase
Own work, CC BY-SA 4.0, Link

By Schneekoala
Own work, CC BY-SA 4.0, Link

If you’re up for a lot more work and fun (maybe the seed of a project idea?), define another function, called scanDesignedBarcode, that recognizes barcodes with some extra flair. The challenge is to recognize as many of the examples above (and others like them) as possible. Unlike the simple, ideal barcode images supported by scanBarcode, this version will need to do much more — to identify which pixels comprise the barcode data, to tolerate noise, and so on.

While you’re at it, you might also design your own designed barcodes, either programmatically with Haskell/HIP, or with a graphic design tool, or some combination of both. If you do, tell us about what you made and share them with the world to enjoy.

Grading

Here is an outline of the grading rubric (10 points total):

Sanity Check

Once you are finished, check that you added, committed, and pushed everything to your repository before the deadline (or within an eligible late period):

https://github.com/UChicago-PL/cs223-fa23-pa-5-hip-barcodes-USERNAME

Misc