diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Henning Thielemann
+
+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 Henning Thielemann 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/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,33 @@
+.SECONDARY:     %.frames
+
+CABALOPTS =
+
+RESOLUTION = 108
+#RESOLUTION = 72
+#RESOLUTION = 54
+
+PAGES = --last-page=1000
+#PAGES = --first-page=100 --last-page=200
+
+ANTIALIAS = -dTextAlphaBits=2 -dGraphicsAlphaBits=2
+
+%.frames:	%.pdf	src/Main.hs
+	mkdir -p /tmp/$*-frames
+	rm -f /tmp/$*-frames/*
+	cabal run gs-parallel $(CABALOPTS) -- \
+	  -j4 $(PAGES) $< "/tmp/$*-frames/%04d.png" -- \
+	  -dNOPAUSE -dBATCH \
+	  -sDEVICE=png16m $(ANTIALIAS) -r$(RESOLUTION)
+	touch $@
+
+# although I request 4 pages and ghostscript claims,
+# that it processed 4 pages, it emits only one.
+# Also happens when rendering the PDF-1.5 reference.
+# It seems that PageList option is seriously broken in gs version 9.53.3 (2020-10-01).
+# Seems to be fixed in version 10.01.2 (2023-06-21), though.
+%0001.png:	%.pdf	src/Main.hs
+	gs -dNOPAUSE -dBATCH \
+	  -sDEVICE=png16m $(ANTIALIAS) \
+	  -dDEVICEHEIGHT=$(RESOLUTION)0 -r$(RESOLUTION) \
+	  -sOutputFile=$*%04d.png -sPageList=1,5,9,13 $<
+#	  -sOutputFile=$*%04d.png -dFirstPage=1000 -dLastPage=1100 $<
diff --git a/Setup.lhs b/Setup.lhs
new file mode 100644
--- /dev/null
+++ b/Setup.lhs
@@ -0,0 +1,3 @@
+#! /usr/bin/env runhaskell
+> import Distribution.Simple
+> main = defaultMain
diff --git a/ghostscript-parallel.cabal b/ghostscript-parallel.cabal
new file mode 100644
--- /dev/null
+++ b/ghostscript-parallel.cabal
@@ -0,0 +1,54 @@
+Name:                ghostscript-parallel
+Version:             0.0
+Synopsis:            Let Ghostscript render pages in parallel
+Description:
+  This is intended for rendering PostScript or PDF documents using Ghostscript.
+  Ghostscript has the option @-dNumRenderingThreads@
+  but it does not speedup pretty much,
+  because it renders each page using multiple threads.
+  .
+  This program runs Ghostscript multiple times on distinct page subsets,
+  such that Ghostscript can render pages in parallel.
+  .
+  @gs-parallel@ has its own options,
+  interprets them and converts and passes them to @gs@,
+  but it also allows to pass custom options to @gs@ after @--@.
+  .
+  Run it like so:
+  .
+  > gs-parallel input.pdf frames-%04d.png \
+  >     -- -sDEVICE=png16m -dNOPAUSE -dBATCH
+Homepage:            https://hub.darcs.net/thielema/ghostscript-parallel
+License:             BSD3
+License-File:        LICENSE
+Author:              Henning Thielemann
+Maintainer:          haskell@henning-thielemann.de
+Category:            Graphics
+Build-Type:          Simple
+Cabal-Version:       >=1.10
+Extra-Source-Files:
+  Makefile
+
+Source-Repository this
+  Tag:         0.0
+  Type:        darcs
+  Location:    https://hub.darcs.net/thielema/ghostscript-parallel
+
+Source-Repository head
+  Type:        darcs
+  Location:    https://hub.darcs.net/thielema/ghostscript-parallel
+
+Executable gs-parallel
+  Build-Depends:
+    pooled-io >=0.0.2 && <0.1,
+    shell-utility >=0.1 && <0.2,
+    optparse-applicative >=0.11 && <0.19,
+    directory >=1.3 && <1.4,
+    process >=1.6 && <1.7,
+    utility-ht >=0.0.10 && <0.1,
+    base >=4.5 && <5
+  Hs-Source-Dirs:      src
+  Main-Is:             Main.hs
+  Default-Language:    Haskell2010
+  GHC-Options:
+    -Wall -fwarn-incomplete-uni-patterns -fwarn-tabs -threaded
diff --git a/src/Main.hs b/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Main.hs
@@ -0,0 +1,124 @@
+module Main where
+
+import qualified Options.Applicative as OP
+
+import Shell.Utility.ParseArgument (parseNumber)
+import Shell.Utility.Exit (exitFailureMsg)
+
+import qualified Control.Concurrent.PooledIO.Independent as Pool
+
+import qualified System.Environment as Env
+import System.Directory (renameFile)
+import System.Process (callProcess, readProcess)
+
+import qualified Data.Foldable as Fold
+import qualified Data.List.HT as ListHT
+import qualified Data.Ix as Ix
+import Data.Tuple.HT (mapSnd)
+import Data.Monoid ((<>))
+
+import Control.Applicative (pure, (<*>))
+
+import Text.Printf (printf)
+
+
+{-
+qpdf --show-npages input.pdf
+
+gs -q -dNODISPLAY --permit-file-read=$PATH_TO_PDF -c "($PATH_TO_PDF) (r) file runpdfbegin pdfpagecount = quit"
+-}
+countPages :: FilePath -> IO Int
+countPages path = do
+   countStr <-
+      readProcess "gs"
+         ["-q", "-dNODISPLAY", "--permit-file-read=" ++ path, "-c",
+          "(" ++ path ++ ") (r) file runpdfbegin pdfpagecount = quit"]
+         ""
+   case reads countStr of
+      [(count, _)] -> return count
+      _ ->
+         exitFailureMsg $
+         "'gs' returned an incomprehensible page count, namely: " ++ countStr
+
+
+-- cf. numeric-prelude
+divUp :: (Integral a) => a -> a -> a
+divUp x y = - div (-x) y
+
+run ::
+   Int -> Int -> Maybe Int ->
+   FilePath -> FilePath -> [String] -> IO ()
+run numJobs firstPage mLastPage input output gsArgs = do
+   lastPage <- maybe (countPages input) return mLastPage
+   let num = Ix.rangeSize (firstPage, lastPage)
+   let ranges =
+         zip [0::Integer ..] $
+         ListHT.mapAdjacent
+            (\p0 p1 -> (p0,p1-1))
+            (map ((firstPage+) . flip divUp numJobs) $
+             take (numJobs+1) $ iterate (num+) 0)
+   Pool.runException (Just numJobs) $
+      flip map ranges $ \(job,(p0,p1)) -> do
+         callProcess "gs" $
+            ("-dFirstPage=" ++ show p0) :
+            ("-dLastPage=" ++ show p1) :
+            (printf "-sOutputFile=%s-%d" output job) :
+            gsArgs ++
+            [input]
+   let pages = do
+         (job,range) <- ranges
+         page <- Ix.range (1, Ix.rangeSize range)
+         return (job, page)
+   Fold.for_ (zip [1::Integer ..] pages) $ \(dstPage, (job, srcPage)) -> do
+      renameFile
+         (printf output srcPage ++ "-" ++ printf "%d" job)
+         (printf output dstPage)
+
+
+parser :: OP.Parser ([String] -> IO ())
+parser =
+   pure run
+   <*>
+      (OP.option
+            (OP.eitherReader $ parseNumber "number of jobs" (0<) "positive") $
+         OP.short 'j' <>
+         OP.long "jobs" <>
+         OP.metavar "NUMBER" <>
+         OP.value 1 <>
+         OP.help "Number of parallel threads")
+   <*>
+      (OP.option (OP.eitherReader $ parseNumber "page" (0<) "positive") $
+         OP.long "first-page" <>
+         OP.metavar "ONEBASED" <>
+         OP.value 1 <>
+         OP.help "First page to render")
+   <*>
+      (OP.option
+            (fmap Just $ OP.eitherReader $ parseNumber "page" (0<) "positive") $
+         OP.long "last-page" <>
+         OP.metavar "ONEBASED" <>
+         OP.value Nothing <>
+         OP.help "Last page to render")
+   <*>
+      OP.strArgument
+         (OP.metavar "INPUT" <>
+          OP.help "Input Document")
+   <*>
+      OP.strArgument
+         (OP.metavar "OUTPUT" <>
+          OP.help "Output Filename Pattern")
+
+info :: OP.Parser a -> OP.ParserInfo a
+info p =
+   OP.info
+      (OP.helper <*> p)
+      (OP.fullDesc <>
+       OP.progDesc "Let Ghostscript render pages in parallel.")
+
+main :: IO ()
+main = do
+   args <- Env.getArgs
+   let (optParseArgs, gsArgs) = mapSnd (drop 1) $ break ("--" ==) args
+   ($gsArgs) =<<
+      OP.handleParseResult
+         (OP.execParserPure OP.defaultPrefs (info parser) optParseArgs)
