diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,5 @@
+# Revision history for comic
+
+## 0  -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2017, davean
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of davean nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/comic.cabal b/comic.cabal
new file mode 100644
--- /dev/null
+++ b/comic.cabal
@@ -0,0 +1,28 @@
+name:                comic
+version:             0
+synopsis:            A format for describing comics.
+-- description:         
+homepage:            https://oss.xkcd.com/
+license:             BSD3
+license-file:        LICENSE
+author:              davean
+maintainer:          oss@xkcd.com
+copyright:           2013-2017
+category:            Data
+build-type:          Simple
+extra-source-files:  ChangeLog.md
+cabal-version:       >=1.10
+
+source-repository head
+  type: git
+  location: https://code.xkrd.net/xkcd/comic.git
+                     
+library
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  exposed-modules:
+          Data.Comic
+  build-depends:
+        base >=4.9 && <4.11
+      , text >= 1.2 && < 1.3
+      , aeson >= 1.1 && < 1.2
diff --git a/src/Data/Comic.hs b/src/Data/Comic.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Comic.hs
@@ -0,0 +1,156 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Data.Comic (
+    Hint(..), Position(..), Font(..), FontName, FontSize, RGBA, Comic(..), Panel(..)
+  , HintSize, ImageName
+  , ComicDelta(..)
+  , blackRGBA
+  ) where
+
+import Control.Monad
+import Control.Applicative
+import Data.Text (Text)
+import qualified Data.Aeson as JS
+import qualified Data.Aeson.Types as JS
+import Data.Aeson (ToJSON(..), FromJSON(..), (.:))
+
+type ImageName = Text
+type FontName = Text
+type FontSize = Int
+type HintSize = Int
+type RGBA = (Float, Float, Float, Float)
+
+blackRGBA :: RGBA
+blackRGBA = (0, 0, 0, 1)
+
+data Font =
+    Font FontName FontSize RGBA
+  deriving (Show, Eq, Ord)
+
+data Position =
+    Pos Int Int
+  deriving (Show, Eq, Ord)
+
+instance ToJSON Position where
+  toJSON (Pos x y) = JS.object [("x", toJSON x), ("y", toJSON y)]
+
+instance FromJSON Position where
+  parseJSON (JS.Object v) = Pos <$> v .: "x" <*> v .: "y"
+  parseJSON _ = mzero
+
+data Hint =
+    ZoneHint {
+        _hintTxt :: Text
+      }
+  | ClickZoneHint {
+        _hintLocal :: Maybe (Position, HintSize)
+      , _hintLink :: Text
+      }
+  | HoverHint {
+        -- Where the hint is, in relation to the slice it is in.
+        _hintPos :: Position
+        -- How far off the target they'll get the hint on hover and how large we highlight.
+      , _hintSize :: HintSize
+        -- What the actual hint is.
+      , _hintTxt :: Text
+      }
+  deriving (Show, Eq, Ord)
+
+instance ToJSON Hint where
+  toJSON (ZoneHint t) = JS.object [("txt",  toJSON t)]
+  toJSON (ClickZoneHint ml l) = JS.object $ [ ("href", toJSON l) ] ++
+                                maybe [] (\(p, s) -> [("size", toJSON s), ("pos", toJSON p)]) ml
+  toJSON (HoverHint p s t) =
+    JS.object
+    [ ("pos", toJSON p)
+    , ("size", toJSON s)
+    , ("txt", toJSON t) ]
+
+instance FromJSON Hint where
+  parseJSON (JS.Object v) =
+    (ClickZoneHint <$> (((\s p -> Just (p, s)) <$> v .: "size" <*> v .: "pos") <|> pure Nothing) <*> v .: "href") <|>
+    (HoverHint <$> v .: "pos" <*> v .: "size" <*> v .: "txt") <|>
+    (ZoneHint <$> v .: "txt")
+  parseJSON _ = mzero
+
+data Panel =
+  Panel {
+    -- List of panel parts, left to right.
+      _panelBackground   :: ImageName
+    -- Zone hints.
+    , _panelHints    :: [Hint]
+    , _panelTexts    :: [((Position, Position), Font, Text)]
+    , _panelOverlays :: [(Position, ImageName)]
+    }
+  deriving (Show, Eq, Ord)
+
+text2json :: ((Position, Position), Font, Text) -> JS.Value
+text2json ((tl, br), Font fn fs c, t) =
+  JS.object [ ("tl", toJSON tl), ("br", toJSON br)
+            , ("font", toJSON fn), ("size", toJSON fs), ("color", toJSON c)
+            , ("txt", toJSON t) ]
+
+json2text :: JS.Value -> JS.Parser ((Position, Position), Font, Text)
+json2text (JS.Object v1) = 
+        (\tl br fn fs c t -> ((tl, br), Font fn fs c, t)) <$>
+          v1 .: "tl" <*> v1 .: "br" <*>
+          v1 .: "font" <*> v1 .: "size" <*> v1 .: "color" <*>
+          v1 .: "txt"
+json2text _ = mzero
+
+instance ToJSON Panel where
+  toJSON (Panel ss hs txts overs) =
+    JS.object $ [
+        ("background", toJSON ss)
+      , ("hints", toJSON hs)
+      , ("texts", toJSON $ map text2json txts)
+      , ("overlays", toJSON $ map (\(p, i) ->
+                                    JS.object [ ("tl", toJSON p)
+                                              , ("img", toJSON i) ]) overs)
+      ]
+
+instance FromJSON Panel where
+  parseJSON (JS.Object v) = 
+      Panel <$> 
+        v .: "background" <*>
+        v .: "hints" <*>
+        (v .: "texts" >>= mapM json2text) <*>
+        (v .: "overlays" >>= mapM parseOver)
+    where
+      parseOver (JS.Object v1) = (,) <$> v1 .: "tl" <*> v1 .: "img"
+      parseOver _ = mzero
+  parseJSON _ = mzero
+
+data Comic =
+  Comic {
+      _comicPanels :: [Panel]
+    , _comicAlt :: Text
+      -- This needs something more to tell us how to layout the panels.
+    , _comicPad    :: Int
+    }
+  deriving (Show, Eq, Ord)
+
+instance ToJSON Comic where
+  toJSON (Comic ps alt pad) = JS.object [("panels", toJSON ps), ("pad", toJSON pad), ("alt", toJSON alt)]
+
+instance FromJSON Comic where
+  parseJSON (JS.Object v) =
+    Comic <$> v .: "panels" <*> v .: "alt" <*> v .: "pad"
+  parseJSON _ = mzero
+
+data ComicDelta =
+   CompleteComic Comic
+ | AppendPanels [Panel]
+ | AddTexts [((Position, Position), Font, Text)]
+ deriving (Show, Eq)
+
+instance ToJSON ComicDelta where
+  toJSON (CompleteComic c) = JS.object [("complete", toJSON c)]
+  toJSON (AppendPanels ps) = JS.object [("append_panels", toJSON ps)]
+  toJSON (AddTexts txts) = JS.object [("add_texts", toJSON $ map text2json txts)]
+
+instance FromJSON ComicDelta where
+  parseJSON (JS.Object v) =
+    (CompleteComic <$> v .: "complete") <|>
+    (AppendPanels <$> v .: "append_panels") <|>
+    (AddTexts <$> (v .: "add_texts" >>= mapM json2text)) 
+  parseJSON _ = mzero
