Between the Lines While doodling around on square-ruled paper the other day, I came up with the above negative-space font for digits. In case you haven't spotted it yet, the spaces between the above shapes yield the golden ratio 1.618033988749. In this challenge, your task is to take a number as input and render it exactly as the example above shows.

Here is how these are created: all lines will be on a regular grid, so that the individual digits are made up of a small number of grid cells. Here are the shapes of the 10 digits (we'll ignore the decimal point for this challenge): Yes, the 7 differs from the golden ratio example at the top. I kinda messed that up. We'll be going with this one.

Notice that each digit is five cells tall, and three cells wide. To render a number, you can imagine placing all its digits next to each other, such that there is exactly one empty column between each pair of digits. For example, taking 319 as input, we'd write: Notice that we add one leading and trailing empty column. Now we invert the cells: The output should then be the boundaries of the resulting polygons: Of course you may generate the result in any other way, as long as the rendered output looks the same.

Input

• You may write a program or function, taking input via STDIN (or closest alternative), command-line argument or function argument, as a string, or list of digits. (You can't take a number as that wouldn't allow you to support leading zeros.)
• You may assume that there will be no more than 16 digits in the input.

Output

• Output may either be displayed on screen or written to a file in a common image format.
• You may use both raster and vector graphics.
• In either case, the aspect ratio of the cells of the underlying grid needs to be 1 (that is, the cells should be squares).
• In the case of raster graphics, each cell should cover at least 20 by 20 pixels.
• The lines must be no wider than 10% of the cell size. I'm willing to give one or two pixels leeway due to aliasing here.
• Lines and background can be any two clearly distinguishable colours, but the shapes created by the lines must not be filled (that is the insides should be the background colour as well).
• There must be no gaps within each closed loop.
• Of course, the entire result must be visible.

Test Cases

Here are 10 inputs, which together cover all possible pairs of adjacent digits, as well as every possible leading and trailing digit:

07299361548
19887620534
21456837709
39284106657
49085527316
59178604432
69471338025
79581224630
89674235011
97518264003

And here are the expected results for those:

Make sure that your code also works when given a single digit (I don't want to include the expected results here, because they should be obvious, and the test case section is bloated enough as it is).

• Would ASCII art be accepted?
– Blue
May 3 '16 at 19:20
• @Blue I think that would make for a very different task, and answers would be hardly comparable, so no, sorry. I'm generally not a fan of mixing graphical output and ASCII art in a single challenge. May 3 '16 at 19:26
• alright, thanks for the quick reply
– Blue
May 3 '16 at 19:28
• Is it allowed to import fonts?
– Marv
May 3 '16 at 21:12
• @Marv hm, interesting question. I'd say you should count the size of the font file in that case. May 3 '16 at 21:14

Octave, 233 225 216 213 bytes

o=@ones;l=z=o(5,1);for k=input('')-47;l=[l,reshape(dec2bin([448,22558,8514,10560,3936,2376,328,15840,320,2368](k),15),5,[])-48,z];end;L=~o(size(l)+2);L(2:6,2:end-1)=l;O=o(3);O(5)=-8;M=~conv2(kron(L,o(25)),O);imshow(M)

Here the first test case (from a resized screen capture, s.t. it fits my monitor=): o=@ones;
l=z=o(5,1);                   %spacer matrices
for k=input('')-47;           %go throu all input digis
%decode the matrices for each digit from decimal
l=[l,reshape(dec2bin([448,22558,8514,10560,3936,2376,328,15840,320,2368](k),15),5,[])-48,z];
end
L(2:6,2:end-1)=l;
O=o(3);O(5)=-8;               %create edge detection filter
imshow(~conv2(kron(L,o(25)),O)) %image resizing /edge detection (change 25 to any cell size you like)

The input can be arbitrary length, like e.g. '07299361548'

Convolution is the key to success.

• Thanks @LuisMendo for improving the score by a bunch of bytes=) May 3 '16 at 23:37
• I couldn't agree more with Convolution is the key to success :-) May 3 '16 at 23:39
• Somehow this is always the motto for my successfull matlab/octave answers :D May 3 '16 at 23:40

Html+JavaScript ES6, 352

Test running the snippet below

<canvas id=C></canvas><script>s=prompt(),C.width=-~s.length*80,c=C.getContext("2d"),[...s].map(d=>[30,d*=3,++d,++d].map(w=a=>{for(a=parseInt("vhvivgtlnllv74vnltvlt11vvlvnlv0"[a],36)*2+1,p=1,y=100,i=64;i>>=1;p=b,y-=20)c.moveTo(x+20,y),b=a&i?1:0,c[b-p?'lineTo':'moveTo'](x,y),(a^q)&i&&c.lineTo(x,y-20);q=a,x+=20}),q=63,x=0),w(30),w(0),c.stroke()</script>

Less golfed

s=prompt(),C.width=-~s.length*80,c=C.getContext("2d"),
w=a=>{
a=parseInt("vhvivgtlnllv74vnltvlt11vvlvnlv0"[i],36)*2+1
for(p=1,y=100,i=32;i;p=b,y-=20,i>>=1)
c.moveTo(x+20,y),
b=a&i?1:0,
c[b-p?'lineTo':'moveTo'](x,y),
(a^q)&i&&c.lineTo(x,y-20)
q=a
x+=20
},
[...s].map(d=>[30,d*=3,++d,++d].map(w),q=63,x=0),
w(30),w(0)
c.stroke()
• I don't think you need the closing script tag... May 5 '16 at 0:09

Javascript ES6, 506 bytes

a=>{with(document)with(body.appendChild(createElementcanvas))with(getContext2d){width=height=(a.length+2)*80;scale(20,20);translate(1,1);lineWidth=0.1;beginPath();["oint",...a.map(i=>"05|7agd7|oint 067128a45|oicgmnt 01de25|oil9amnt 01de23fg45|oint 03fh5|68ec6|oint 03fg45|oij78knt 05|9agf9|oij78knt 01dh5|oint 05|78ed7|9agf9|oint 03fg45|78ed7|oint".split [i]),"05"].map(i=>{i.split|.map(i=>[...i].map((e,i,_,p=parseInt(e,36),l=~~(p/6),r=p%6)=>i?lineTo(l,r):moveTo(l,r)));translate(4,0)});stroke()}}

Ungolfed:

a=>{                                            // anonymous function declaration, accepts array of numbers
with(document)                                // bring document into scope
with(body.appendChild(createElementcanvas)) // create canvas, drop into html body, bring into scope
with(getContext2d){                         // bring graphics context into scope
width=height=(a.length+2)*80;               // set width and height
scale(20,20);                               // scale everything to 20x
lineWidth=0.1;                              // have to scale line width since we scaled 20x
beginPath();                                // start drawing lines
["oint",                                    // beginning "glyph", draws left end of negative space, see below
...a.map(i=>05|7agd7|oint                 // glyphs 0-9 encoded as vertices
067128a45|oicgmnt             //   glyphs seperated by " "
01de25|oil9amnt               //   lines within each glyph seperated by "|"
01de23fg45|oint               //   a single vertex is stored as a base36 char
03fh5|68ec6|oint              //     where a number corresponds to one of the verts shown below:
03fg45|oij78knt               //        0  6 12 18 24
05|9agf9|oij78knt             //        1  7 13 19 25
01dh5|oint                    //        2  8 14 20 26
05|78ed7|9agf9|oint           //        3  9 15 21 27
03fg45|78ed7|oint            //        4 10 16 22 28
.split [i]),                           //        5 11 17 23 29
"05"]                                      // end "glyph", draws right end of negative space, see above
.map(i=>{                                 // for each glyph string
i.split|                              // seperate into list of line strings
.map(i=>[...i]                        // convert each line string into list of chars
.map((e,i,_,p=parseInt(e,36),       // convert base36 char to number
l=~~(p/6),r=p%6)=>            // compute x y coords of vertex
i?lineTo(l,r):moveTo(l,r)));      // draw segment
translate(4,0)});                       // translate origin 4 units to right
stroke()}}                                  // draw all lines to canvas

Assumes there's a <body> to append the canvas to, tested in Firefox 46.

Example run (assigning anonymous function to f):

f([1,0,3])

yields: Java, 768 bytes

import java.awt.*;import java.awt.image.*;class G{public static void main(String[]v)throws Exception{int s=20,n=v.length(),i=0,j,w=(n*3+n+1)*s,h=5*s,a[][]={{6,7,8},{0,2,3,10,11,12,13},{1,6,8,13},{1,3,6,8},{3,4,5,6,8,9},{3,6,8,11},{6,8,11},{1,2,3,4,6,7,8,9},{6,8},{3,6,8}};BufferedImage o,b=new BufferedImage(w,h,1);Graphics g=b.getGraphics();g.setColor(Color.WHITE);for(;i<n;i++)for(j=0;j<15;j++){int c=j;if(java.util.Arrays.stream(a[v.charAt(i)-48]).noneMatch(e->e==c))g.fillRect((1+i*4+j/5)*s,j%5*s,s,s);}o=new BufferedImage(b.getColorModel(),b.copyData(null),0>1,null);for(i=1;i<h-1;i++)for(j=1;j<w-1;j++)if((b.getRGB(j+1,i)|b.getRGB(j-1,i)|b.getRGB(j,i+1)|b.getRGB(j,i-1))<-1)o.setRGB(j,i,-1);javax.imageio.ImageIO.write(o,"png",new java.io.File("a.png"));}}

Ungolfed

import java.awt.*;
import java.awt.image.BufferedImage;

class Q79261 {
public static void main(String[] v) throws Exception {
int scale = 20, n = v.length(), i = 0, j, width = (n * 3 + n + 1) * scale, height = 5 * scale, values[][] = {{6, 7, 8}, {0, 2, 3, 10, 11, 12, 13}, {1, 6, 8, 13}, {1, 3, 6, 8}, {3, 4, 5, 6, 8, 9}, {3, 6, 8, 11}, {6, 8, 11}, {1, 2, 3, 4, 6, 7, 8, 9}, {6, 8}, {3, 6, 8}};
BufferedImage output, temp = new BufferedImage(width, height, 1);
Graphics g = temp.getGraphics();
g.setColor(Color.WHITE);
for (; i < n; i++)
for (j = 0; j < 15; j++) {
int finalJ = j;
if (java.util.Arrays.stream(values[v.charAt(i) - 48]).noneMatch(e -> e == finalJ))
g.fillRect((1 + i * 4 + j / 5) * scale, j % 5 * scale, scale, scale);
}
output = new BufferedImage(temp.getColorModel(), temp.copyData(null), 0 > 1, null);
for (i = 1; i < height - 1; i++)
for (j = 1; j < width - 1; j++)
if ((temp.getRGB(j + 1, i) | temp.getRGB(j - 1, i) | temp.getRGB(j, i + 1) | temp.getRGB(j, i - 1)) < -1)
output.setRGB(j, i, -1);
javax.imageio.ImageIO.write(output, "png", new java.io.File("a.png"));
}
}

Notes

• Input is a single string as argument. How to use: javac G.java, java G 80085

• I'm starting with a black canvas, then I'm adding the numbers as white positives. I create a copy of the image and then flip every black pixel that has 4 black neighbours on the original image.

Outputs

Some single digits:

• java.awt.* does not contain java.awt.image.BufferedImage? May 4 '16 at 22:16
• @Element118 it does not.
– Marv
May 4 '16 at 22:17

R, too many bytes to golf (1530+ 1115)

library(reshape2);library(ggplot2);library(png)
M=matrix(1,5,3)
M=lapply(list(c(7:9),c(1,3,4,11:14),c(2,7,9,14),c(2,4,7,9),c(4:7,9,10),c(4,7,9,12),c(7,9,12),c(2:5,7:10),c(7,9),c(4,7,9)),function(x){M[x]=0;M})
g=function(P){
S=matrix(0,5,1)
R=NULL
for(N in P){R=Reduce(cbind2,list(R,S,M[[N+1]]))}
cbind(R,S)}
p=function(P){
o=t(apply(g(P),1,rev))
ggplot(melt(o),aes(x=Var1,y=Var2))+geom_raster(aes(fill=value))+coord_flip()+scale_fill_continuous(guide=FALSE,high="#FFFFFF",low="#000000")+scale_y_reverse()+scale_x_reverse()+theme_bw()+theme(panel.grid=element_blank(),panel.border=element_blank(),panel.background=element_blank(),axis.title=element_blank(),axis.text=element_blank(),axis.ticks=element_blank(),plot.margin=unit(c(0,0,0,0),"mm"))+ggsave("t.png",width=dim(o)/2.5,height=2,units="in",dpi=99)
k=q[,,1]
b=replace(k,k==1,0)
for(i in 1:nrow(k)){
for(j in 1:ncol(k)){
u=(i==nrow(k))
v=(j==ncol(k))
if(u&v){b[i,j]=1;break}
if((i==1)|u|(j==1)|v){b[i,j]=1;next}else{if(all(k[c((i-1):(i+1)),c((j-1):(j+1))])){b[i,j]=1}else{b[i,j]=0}}}}
q[,,1:3]=abs(replace(k,b==1,0)-1)
writePNG(q,"t.png")}

# run p(c(0,1,2,3,4,5))

lol at writing to disk, then reading from disk edit away the black fill.

Python 3, 326 325 bytes

import numpy
from skimage import io,transform as t,filters as f
r=[*5,*5]
for c in map(int,input()):r+=[map(float,bin(0x3f1fa7e1bd7b5aff84ff6b7fd6f087ff5ff6bf)[2:][15*c+5*i:15*c+5*-~i])for i in[0,1,2]]+[*5]
r+=[*5]
p=[*len(r)]
r=p+list(zip(*r))+p
io.imsave("o.png",1-f.sobel((t.rescale(numpy.array(r),16,0))))
• I was going to comment this on one of your other answers the other day, but range(3) is never worth it. May 4 '16 at 13:58

BBC BASIC, 182 ASCII characters (tokenised filesize 175 bytes)

I.n$F.j=0TOLENn$*4p=ASCM."?@\@?[@_?DTP?TT@?pv@?PTD?@TD?||@?@T@?PT@",VALM.n$,j/4+1,1)*4+1+j MOD4)F.k=0TO4p*=2q=64A.p V.537;q;0;2585;0;q;537;-q;0;2585;0;-q;25;0;64; N.MOVEBY 64,-320N. Scoring: When the above program is pasted into the editor and run, the editor will expand the abbreviated keywords into full keywords onscreen, though they are actually only 1 byte after tokenisation. (Example I.=INPUT storage space 1 byte.) Explanation I will just explain what the VDU line does: it draws a box by bit-flipping the current pixel colour on the screen. This means that (with a bit of care with the corners) it is possible simply to draw one cell next to each other, and the intervening edge will cancel out and disappear due to double drawing. Close examination will reveal that the top right and bottom left corners of a cell are drawn but the top left and bottom right are missing ("rounded") in order to make this work. After the cell is drawn, the graphics cursor is moved up 32 pixels ready for the next cell to be drawn. The rest of the program is a fairly straightforward ASCII bitmap decompression. The dimensions of the cell are 64x64 units for golfing/compatability with the way the bitmap is decompressed. q controls the size of the cell that is plotted: 64x64 units for a cell that is present, 0x0 for a cell that is absent. Ungolfed code m$="?@\@?[@_?DTP?TT@?pv@?PTD?@TD?||@?@T@?PT@" :REM bitmap for digits, including column of filled cells at left. BBC strings are 1-indexed
INPUTn$:REM user input FORj=0 TO LENn$*4                             :REM iterate 4 times per input digit, plus once more (plot column 0 of imaginary digit to finish)
d=VAL MID$(n$,j/4+1,1)                      :REM extract digit from n$(1-character string). VAL of empty string = 0, so 123->1,1,1,1,2,2,2,2,3,3,3,3,0 p=ASC MID$(m$,d*4+1+j MOD4) :REM get column bitmap from m$ d*4 selects digit, j MOD4 selects correct column of digit, add 1 to convert to 1-index
FORk=0TO4                                   :REM for each cell in the column
p*=2                                      :REM bitshift p
q=64ANDp                                  :REM find size of cell to draw. 64 for a filled cell, 0 for an absent cell.
VDU537;q;0;                               :REM line q units right, inverting existing screen colour. Draw last pixel (will be inverted next line)
VDU2585;0;q;                              :REM line q units up, inverting existing screen colour. Dont draw last pixel (will be filled in next line)
VDU537;-q;0;                              :REM line q units left, inverting existing screen colour. Draw last pixel (will be inverted next line)
VDU2585;0;-q;                             :REM line q units down, inverting existing screen colour. Dont draw last pixel (avoid inverting 1st pixel of 1st line)
VDU25;0;64;                               :REM move up 64 units for cell above
NEXT
MOVEBY 64,-320                              :REM move right and down for next column.
NEXT

Output

The MOVEs are just get the output to appropriate heights on the screen. BBC basic uses 2 units = 1 pixel in this mode, so the cells are actually 32x32 pixels. C#, 768 773 776 bytes

namespace System.Drawing{class P{static void Main(string[]a){uint[]l={0xEBFBFFFC,0xB89B21B4,0xABFFF9FC,0xAA1269A4,0xFFF3F9FC};var w=a.Length*80+20;var b=new Bitmap(w,100);var g=Graphics.FromImage(b);g.FillRectangle(Brushes.Black,0,0,w,100);for(int i=0;i<a.Length;i++)for(int r=0;r<5;r++)for(int c=0;c<3;c++)if((l[r]&((uint)1<<(175-a[i]*3-c)))>0)g.FillRectangle(Brushes.White,20*(1+i*4+c),20*r,20,20);for(int x=1;x<w-1;x++)for(int y=1;y<99;y++)if(b.GetPixel(x,y).B+b.GetPixel(x+1,y).B+b.GetPixel(x,y+1).B+b.GetPixel(x,y-1).B+b.GetPixel(x+1,y-1).B+b.GetPixel(x+1,y+1).B+b.GetPixel(x-1,y+1).B+b.GetPixel(x-1,y-1).B==0)b.SetPixel(x,y,Color.Red);for(int x=1;x<w-1;x++)for(int y=1;y<99;y++)if(b.GetPixel(x,y).R>0)b.SetPixel(x,y,Color.White);b.Save(a+".bmp");}}}

Takes the number as command line argument. Outputs a nice, clean, non-aliased BMP image with the number as the name.

Original before golfing:

namespace System.Drawing
{
class P
{
static void Main(string[] args)
{
var numbers = args;
uint[] lines = {
0xEBFBFFFC, // 111 010 111 111 101 111 111 111 111 111 00
0xB89B21B4, // 101 110 001 001 101 100 100 001 101 101 00
0xABFFF9FC, // 101 010 111 111 111 111 111 001 111 111 00
0xAA1269A4, // 101 010 100 001 001 001 101 001 101 001 00
0xFFF3F9FC  // 111 111 111 111 001 111 111 001 111 111 00
};
var width = numbers.Length*4 + 1;
var bmp = new Bitmap(width*20, 5*20);
using (var gfx = Graphics.FromImage(bmp))
{
gfx.FillRectangle(Brushes.Black, 0, 0, width*20+2, 5*20+2);
// Process all numbers
for (int i = 0; i < numbers.Length; i++)
{
var number = numbers[i]-'0';
for (int line = 0; line < 5; line++)
{
for (int col = 0; col < 3; col++)
{
if ((lines[line] & ((uint)1<<(31-number*3-col))) >0)
gfx.FillRectangle(Brushes.White, 20*(1 + i * 4 + col), 20*line, 20 , 20 );
}
}
}
// Edge detection
for (int x = 1; x < width*20-1; x++)
{
for (int y = 1; y < 5*20-1 ; y++)
{
if (bmp.GetPixel(x,y).B +
bmp.GetPixel(x + 1, y).B +
bmp.GetPixel(x, y + 1).B +
bmp.GetPixel(x, y - 1).B +
bmp.GetPixel(x + 1, y - 1).B +
bmp.GetPixel(x + 1, y + 1).B +
bmp.GetPixel(x - 1, y + 1).B +
bmp.GetPixel(x - 1, y - 1).B == 0)
bmp.SetPixel(x, y, Color.Red);
}
}
// Convert red to white
for (int x = 1; x < width * 20 - 1; x++)
{
for (int y = 1; y < 5 * 20 - 1; y++)
{
if (bmp.GetPixel(x, y).R>0)
bmp.SetPixel(x, y, Color.White);
}
}
}
bmp.Save(@"c:\tmp\test.bmp");
}
}
}

Mathematica 328 bytes

w["07299361548"]
w["19887620534"] Explanation

Four bits will be used in each of 5 lines of cells for each input digit.

"75557262277174771717557117471774757711117575775717"represents 0 to 9 as bitmaps.

The first 5 digits in the large integer above, namely 75557 indicate how each array row for zero should be displayed. 7 will represent {0,1,1,1}, that is, a white cell, followed, on its right, by 3 black cells; the leading 0 is a blank space to separate displayed digits. 5 corresponds to {0,1,0,1}, that is white, black, white, black cells.

The following produces a list of replacement rules:

Thread[ToString /@ Range[0, 9] -> StringPartition["75557262277174771717557117471774757711117575775717", 5]]

{"0" -> "75557", "1" -> "26227", "2" -> "71747", "3" -> "71717", "4" -> "55711", "5" -> "74717", "6" -> "74757", "7" -> "71111", "8" -> "75757", "9" -> "75717"}

Note that when 3 is input, it will be replaced by 71717 This representation is expressed in binary:

p = Partition[IntegerDigits[FromDigits["3" /. {"3" -> "71717"}, 16], 2, 20], 4]

{{0, 1, 1, 1}, {0, 0, 0, 1}, {0, 1, 1, 1}, {0, 0, 0, 1}, {0, 1, 1, 1}}

Its black-white inverse is found by simply exchanging the 1s and 0s.

q = p /. {0 -> 1, 1 -> 0}

{{1, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}}

Let's see what p and q look like when displayed by ArrayPlot:

ArrayPlot[#, Mesh -> True, ImageSize -> Small, PlotLegends -> Automatic] & /@ {p, q} This simply joins the arrays of zeros and ones for each digit before rendering the big array via ArrayPlot. * is defined in j as the final vertical space after the last digit.

Thread[Join @@ Transpose /@ j /@ Characters@(s <> "*")]