Commits

Anonymous committed 7d14705

Initial commit.

Comments (0)

Files changed (9)

+Copyright (C) 2012 Jonas Höglund
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ ini2png
+=========
+
+**ini2png** is a tool to convert images from a textual representation to png
+images.  This could be handy for e.g. smaller pieces of pixel art.
+
+
+## Example
+*ini2png* provides a command-line tool with the same name.
+
+    # npm install -g ini2png
+    [snip]
+    $ ini2png test.txt
+    Done!
+    $ feh test.png
+
+## Format
+The format that *ini2png* understands is based on the 'ini' configuration file
+format.  It consists of two sections, `Colour` and `Content`.
+
+All whitespace-only lines, as well as all lines beginning with a `#`, are
+ignored.  The `#` has to be the *first* character of the line for it to be
+ignored.
+
+### Colour
+The *Colour* section consists of key-value pairs of the form "key = value",
+where each key is a single (non-whitespace) character, and each value is a CSS
+colour.
+
+Currently, only the formats `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa` as well as a
+select subset of "named colours" (e.g. "transparent", "black", "red").
+
+The *Colour* section basically makes up the palette of the image.
+
+### Content
+The *Content* section consists of the actual image.  Each character in this
+section is replaced with the corresponding colour declared in the *Colour*
+section.  Leading whitespace is removed from each line, but only up to the point
+so that each line begins at the same column.  Lines are padded so that they all
+have equal length.
+
+The space character (` `) is special, and represents transparent pixels.
+
+#### Example
+    .......xxxx
+    ...xxxxxxxxxxx
+
+is treated the same as
+
+    .....xxxx...
+    xxxxxxxxxxxx
+
+...where dots represent spaces.

examples/small.png

Added
New image
+# Example image file.
+
+[Colours]
+# Might want to change these colours.. especially `.` to transparent.
+. = black
+x = #f60
+
+[Content]
+   ........................
+   ........................
+   ........................
+
+## CPU thingy
+   ......xx..xx..xx........
+   ......x........x........
+   ........xxxxxx..........
+   ........xxxxxx..........
+   ......x.xxxxxx.x........
+   ......x.xxxxxx.x........
+   ........xxxxxx..........
+   ........xxxxxx..........
+   ......x........x........
+   ......xx..xx..xx........
+
+   ........................
+   ........................
+   ........................
+
+## Clock
+   .........xxxx...........
+   .......xxxxxxxx.........
+   ......xxxxxx..xx........
+   ......xxxxxx.xxx........
+   .....xxxxxx.xxxxx.......
+   .....xxxxx..xxxxx.......
+   .....xxxxx......x.......
+   .....xxxxxxxxxxxx.......
+   ......xxxxxxxxxx........
+   ......xxxxxxxxxx........
+   .......xxxxxxxx.........
+   .........xxxx...........
+
+   ........................
+   ........................
+   ........................
+
+## Smaller clock
+   .........xxxx...........
+   .......xxxxxxxx.........
+   .......xxxxx.xx.........
+   ......xxxxxx.xxx........
+   ......xxxx..xxxx........
+   ......xxxx.....x........
+   ......xxxxxxxxxx........
+   .......xxxxxxxx.........
+   .......xxxxxxxx.........
+   .........xxxx...........
+
+   ........................
+   ........................
+   ........................
+
+## Fan?
+   ........xxx.............
+   ..........xx............
+   ..........xxx....x......
+   ...........xx....x......
+   ............x..xxx......
+   ........xxx...xxx.......
+   .......xxx...xxx........
+   ......xxx..x............
+   ......x....xx...........
+   ......x....xxx..........
+   ............xx..........
+   .............xxx........
+
+   ........................
+   ........................
+   ........................
+
+## Sine wave
+   ........................
+   .............xx.........
+   ............x..x........
+   ........x..x............
+   .........xx.............
+   ........................
+
+   ........................
+   ........................
+   ........................
+
+## Braces! :3
+   .........xx..xx.........
+   ........x......x........
+   ........x......x........
+   ........x......x........
+   .......x........x.......
+   ........x......x........
+   ........x......x........
+   ........x......x........
+   .........xx..xx.........
+
+   ........................
+   ........................
+   ........................
+
+## {F}
+   ........xx.....xx.......
+   .......x.........x......
+   .......x...xxxx..x......
+   .......x...x.....x......
+   ......x....xxx....x.....
+   .......x...x.....x......
+   .......x...x.....x......
+   .......x.........x......
+   ........xx.....xx.......
+
+   ........................
+   ........................
+   ........................
+
+## Box with a diagonal line...
+   ........xxxxxxxxx.......
+   ........x.......x.......
+   ........x.xxxxx.x.......
+   ........x.xxx...x.......
+   ........x.x...x.x.......
+   ........x...xxx.x.......
+   ........x.xxxxx.x.......
+   ........x.......x.......
+   ........xxxxxxxxx.......
+
+   ........................
+   ........................
+   ........................
+
+## Something..
+   ........xx.....xx.......
+   ......x.x.x...x.x.x.....
+   ......x.x.......x.x.....
+   ....x.x.x..xxx..x.x.x...
+   ....x.x.x.xxxxx.x.x.x...
+   ....x.x.x.xxxxx.x.x.x...
+   ....x.x.x..xxx..x.x.x...
+   ......x.x.......x.x.....
+   ......x.x.x...x.x.x.....
+   ........xx.....xx.......
+
+   ........................
+   ........................
+   ........................
+{ "name"         : "ini2png"
+, "version"      : "0.1.0"
+, "author"       : "Jonas Höglund <node@firefly.nu>"
+, "description"  : "tool to convert images from a textual representation to png."
+, "license"      : "MIT"
+
+, "contributors" :
+  [ { "name":"Jonas Höglund", "email":"node@firefly.nu" }
+  ]
+
+, "keywords"     : ["image", "text", "png", "convert"]
+, "dependencies" :
+  { "png"        : "2.x.x"
+  }
+
+, "preferGlobal" : "true"
+, "bin"          : { "ini2png" : "./src/runner.js" }
+, "main"         : "./src/ini2png.js"
+}
+
+var assert = require('assert')
+  , buffer = require('buffer')
+
+var namedColours =
+  { "transparent" : new buffer.Buffer([   0,   0,   0, 255 ])
+  , "red"         : new buffer.Buffer([ 255,   0,   0,   0 ])
+  , "green"       : new buffer.Buffer([   0, 255,   0,   0 ])
+  , "blue"        : new buffer.Buffer([   0,   0, 255,   0 ])
+  , "black"       : new buffer.Buffer([   0,   0,   0,   0 ])
+  , "white"       : new buffer.Buffer([ 255, 255, 255,   0 ])
+  }
+
+// Parses a CSS colour into an RGBA buffer.  Currently only supports `#rgb`,
+// `#rgba`, `#rrggbb`, `#rrggbbaa` or the named colours "transparent", "red",
+// "green", "blue", "black" and "white".  Yes, I'm lazy.  I'm not even sure if
+// you can specify alpha with '#' notation in CSS, but whatever.
+exports.toRGBA = function(colour) {
+  if (colour[0] == '#') {
+    // Handle hex notation (#rgb, #rgba, #rrggbb, #rrggbbaa).
+    var hexpart = colour.slice(1)
+      , rgb
+      , alpha
+
+    switch (hexpart.length) {
+      case 3:
+        rgb   = hexpart.split("").map(twice).map(hexToNum)
+        alpha = 0x0
+        break
+
+      case 4:
+        rgba  = hexpart.split("").map(twice).map(hexToNum)
+        rgb   = rgba.slice(0, 3)
+        alpha = rgba[3]
+        break
+
+      case 6:
+        rgb   = hexpart.match(/../g).map(hexToNum)
+        alpha = 0x0
+        break
+
+      case 8:
+        rgba  = hexpart.match(/../g).map(hexToNum)
+        rgb   = rgba.slice(0, 3)
+        alpha = rgba[3]
+        break
+
+      default:
+        throw new Error("Invalid colour: '" + colour + "'")
+    }
+
+    return new buffer.Buffer(rgb.concat(alpha))
+
+  } else if (colour in namedColours) {
+    // Handle named colours, e.g. "black".
+    return namedColours[colour]
+
+  } else {
+    throw new Error("Invalid colour: '" + colour + "'")
+  }
+
+  // Helper functions
+  function twice(chr) { return chr + chr }
+  function hexToNum(hex) { return parseInt(hex, 16) }
+}
+
+
+var assert = require('assert')
+
+// Parses key-value pairs from an array of lines.  Returns an object that maps
+// keys to values.
+exports.parseKeyValues = function(lines) {
+  var res = {}
+
+  lines.forEach(function(line) {
+    var match = /^\s*(\S+)\s*[=:]\s*(.*)\s*$/.exec(line)
+    assert.ok(match, "Syntax error: invalid key-value pair: '" + line + "'.")
+
+    var key   = match[1]
+      , value = match[2]
+
+    res[key] = value
+  })
+
+  return res
+}
+
+// Parses sections in an ini-like file, ignoring empty & commented lines.
+exports.parseSections = function(str) {
+  var lines   = str.split("\n")
+    , section = null
+    , res     = {}
+    , linesInSection = []
+    , match
+
+  lines.forEach(function(line) {
+    if (match = line.match(/^\[(.*)\]$/)) {
+      maybeAddSection()
+      section = match[1].trim()
+
+    } else if (/^#|^\s*$/.test(line)) {
+      // Skip commented lines or empty lines
+
+    } else {
+      linesInSection.push(line)
+    }
+  })
+
+  maybeAddSection()
+
+  return res
+
+  // Helper functions
+  function maybeAddSection() {
+    if (section != null) {
+      res[section]   = linesInSection
+      linesInSection = []
+    }
+  }
+}
+#!/usr/bin/env node
+
+var buffer     = require('buffer')
+  , png        = require('png')
+  , cssColours = require('./css-colours')
+  , ini        = require('./ini-parser')
+
+// Converts a "text-image" into an image object; see `parseImageData`.
+exports.text2image = function(text) {
+  var sections       = ini.parseSections(text)
+    , coloursSection = ini.parseKeyValues(sections['Colours'])
+
+  // Let spaces represent transparent.
+  coloursSection[" "] = "transparent"
+
+  var colours = mapProperties(coloursSection, cssColours.toRGBA)
+    , image   = parseImageData(sections['Content'], colours)
+
+  return image
+}
+
+// Converts a "text-image" directly into a png buffer, and passes it to a
+// provided callback.
+exports.text2png = function(text, callback) {
+  var image   = exports.text2image(text)
+    , pngObj  = new png.Png(image.buffer, image.width, image.height,
+                            image.bufferType)
+
+  pngObj.encode(callback)
+}
+
+// Maps a function over each property in an object, and returns a new object.
+// Useful for objects used as maps.
+function mapProperties(obj, fun) {
+  var res = {}
+
+  for (var key in obj) {
+    res[key] = fun(obj[key])
+  }
+
+  return res
+}
+
+// Parses image string data into an object representing the image (metadata &
+// rgba buffer).
+function parseImageData(lines, colours) {
+  var longWidth = Math.max.apply(null, lines.map(strLength))
+    , offset    = Math.min.apply(null, lines.map(leadingLength))
+    , width     = longWidth - offset
+    , height    = lines.length
+
+    , buf       = new buffer.Buffer(width * height * 4)
+
+  lines.forEach(function(line, y) {
+    var lineArr = padSpaces(line.slice(offset), width).split("")
+    lineArr.forEach(function(chr, x) {
+      // For each pixel, copy the colours of that pixel to the resulting RGBA
+      // buffer.
+      var colour    = colours[chr]
+        , bufOffset = (y * width + x) * 4
+
+      // This is the actual copying...
+      buf[bufOffset    ] = colour[0]
+      buf[bufOffset + 1] = colour[1]
+      buf[bufOffset + 2] = colour[2]
+      buf[bufOffset + 3] = colour[3]
+    })
+  })
+
+  return (
+    { width      : width
+    , height     : height
+    , bufferType : 'rgba'
+    , buffer     : buf
+    })
+
+  function padSpaces(str, len) {
+    if (str.length < len) {
+      var extras = Array(len - str.length + 1).join(" ")
+      return str + extras
+    } else {
+      return str
+    }
+  }
+
+  function strLength(str) { return str.length }
+  function leadingLength(str) { return /^ */.exec(str)[0].length }
+}
+
+#!/usr/bin/env node
+
+var fs  = require('fs')
+  , i2p = require('./ini2png')
+
+// Check that we've received exactly one argument.
+if (process.argv.length != 3) {
+  console.warn("Usage: " + process.argv[1] + " <filename>")
+  process.exit(1)
+}
+
+var filename = process.argv[2]
+  , outname  = filename.replace(/\.txt$|$/, '.png')
+
+// Read & process file.
+fs.readFile(process.argv[2], function(err, buf) {
+  if (err) throw err
+
+  // Output!
+  i2p.text2png(String(buf), function(pngData) {
+    fs.writeFileSync(outname, pngData)
+    console.log("Done!")
+  })
+})
+