diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Changelog for `JuicyPixels-jpeg-turbo`
+
+## Unreleased
+
+## 0.1.0.0 - YYYY-MM-DD
diff --git a/JuicyPixels-jpeg-turbo.cabal b/JuicyPixels-jpeg-turbo.cabal
new file mode 100644
--- /dev/null
+++ b/JuicyPixels-jpeg-turbo.cabal
@@ -0,0 +1,89 @@
+cabal-version: 2.2
+
+-- This file has been generated from package.yaml by hpack version 0.38.0.
+--
+-- see: https://github.com/sol/hpack
+
+name:           JuicyPixels-jpeg-turbo
+version:        0.1.0.0
+synopsis:       JuicyPixels wrappers for libjpeg-turbo
+description:    NB. You must set one of the build flags on `jpeg-turbo` dependency.
+category:       Codec, Image
+author:         IC Rainbow
+maintainer:     aenor.realm@gmail.com
+copyright:      2024 IC Rainbow
+license:        BSD-3-Clause
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    CHANGELOG.md
+
+source-repository head
+  type: git
+  location: https://gitlab.com/dpwiz/hs-jpeg-turbo
+
+library
+  exposed-modules:
+      Codec.Picture.JPEGTurbo
+  other-modules:
+      Paths_JuicyPixels_jpeg_turbo
+  autogen-modules:
+      Paths_JuicyPixels_jpeg_turbo
+  hs-source-dirs:
+      src
+  default-extensions:
+      BangPatterns
+      BlockArguments
+      ImportQualifiedPost
+      LambdaCase
+      NamedFieldPuns
+      OverloadedStrings
+      PatternSynonyms
+      RecordWildCards
+      TupleSections
+      TypeApplications
+      TypeOperators
+      TypeFamilies
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+  build-depends:
+      JuicyPixels
+    , base >=4.7 && <5
+    , bytestring
+    , jpeg-turbo
+    , vector
+  default-language: Haskell2010
+
+test-suite JuicyPixels-jpeg-turbo-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      JPTest
+      Paths_JuicyPixels_jpeg_turbo
+  autogen-modules:
+      Paths_JuicyPixels_jpeg_turbo
+  hs-source-dirs:
+      test
+  default-extensions:
+      BangPatterns
+      BlockArguments
+      ImportQualifiedPost
+      LambdaCase
+      NamedFieldPuns
+      OverloadedStrings
+      PatternSynonyms
+      RecordWildCards
+      TupleSections
+      TypeApplications
+      TypeOperators
+      TypeFamilies
+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      JuicyPixels
+    , JuicyPixels-jpeg-turbo
+    , base >=4.7 && <5
+    , bytestring
+    , jpeg-turbo
+    , tasty
+    , tasty-discover
+    , tasty-hunit
+  default-language: Haskell2010
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2024 IC Rainbow
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1.  Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+2.  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.
+
+3.  Neither the name of the copyright holder nor the names of its 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 HOLDER 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/src/Codec/Picture/JPEGTurbo.hs b/src/Codec/Picture/JPEGTurbo.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Picture/JPEGTurbo.hs
@@ -0,0 +1,42 @@
+module Codec.Picture.JPEGTurbo
+  ( encodeRGB
+  , EncodableRGB(..)
+  ) where
+
+import Codec.Picture (Image(..), Pixel, PixelBaseComponent, PixelRGB8)
+import Control.Exception (bracket, finally)
+import Control.Monad (unless)
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as B
+import Data.Vector.Storable qualified as V
+import Data.Word (Word8)
+import Foreign (castPtr, peek, with, nullPtr)
+import Foreign.C.String (peekCString)
+import TurboJPEG2 qualified as TJ2
+
+class (Pixel px, PixelBaseComponent px ~ Word8) => EncodableRGB px where
+  jtPixelFormat :: Image px -> TJ2.TJPF
+
+instance EncodableRGB PixelRGB8 where
+  jtPixelFormat _ = TJ2.TJPF_RGB
+
+-- XXX: Use tjCompressFromYUV instead
+-- instance EncodableRGB PixelYCbCr8 where
+--   jtPixelFormat _ = TJ2.TJPF_RGB
+
+encodeRGB :: EncodableRGB px => Int -> Image px -> IO ByteString
+encodeRGB jpegQual image@Image{imageWidth, imageHeight, imageData} =
+  bracket TJ2.tjInitCompress TJ2.tjDestroy \jt ->
+    V.unsafeWith imageData \bufPtr ->
+      with nullPtr \jpegBufPtr ->
+        with 0 \jpegSizePtr -> do
+          rc <- TJ2.tjCompress2 jt (castPtr bufPtr) (fromIntegral imageWidth) 0 (fromIntegral imageHeight) pixelFormat jpegBufPtr jpegSizePtr jpegSubsamp (fromIntegral jpegQual) flags
+          jpegBuf <- peek jpegBufPtr
+          flip finally (TJ2.tjFree jpegBuf) do
+            unless (rc == 0) $ peekCString (TJ2.tjGetErrorStr2 jt) >>= error
+            jpegSize <- peek jpegSizePtr
+            B.packCStringLen (castPtr jpegBuf, fromIntegral jpegSize)
+  where
+    flags = TJ2.TJFLAG_PROGRESSIVE <> TJ2.TJFLAG_STOPONWARNING
+    pixelFormat = jtPixelFormat image
+    jpegSubsamp = TJ2.TJSAMP_420
diff --git a/test/JPTest.hs b/test/JPTest.hs
new file mode 100644
--- /dev/null
+++ b/test/JPTest.hs
@@ -0,0 +1,36 @@
+module JPTest where
+
+import Codec.Picture (DynamicImage(..), convertRGB8, readImageWithMetadata)
+import Codec.Picture.JPEGTurbo qualified as JT
+import Data.ByteString qualified as B
+
+unit_rgb :: IO ()
+unit_rgb = do
+  (imgIn, metaIn) <- readImageWithMetadata inFile >>= either error pure
+  print (pixelFormat imgIn, metaIn)
+  JT.encodeRGB 1 (convertRGB8 imgIn) >>= B.writeFile outFile
+  (imgOut, metaOut) <- readImageWithMetadata outFile >>= either error pure
+  print (pixelFormat imgOut, metaOut)
+  where
+    inFile = "../fixtures/lv.png"
+    outFile = "../fixtures/lv-out.jpg"
+
+-- TODO: unit_rgba etc
+-- TODO: unit_yuv
+
+pixelFormat :: DynamicImage -> String
+pixelFormat = \case
+  ImageY8 _ -> "Pixel8"
+  ImageY16 _ -> "Pixel16"
+  ImageY32 _ -> "Pixel32"
+  ImageYF _ -> "PixelF"
+  ImageYA8 _ -> "PixelYA8"
+  ImageYA16 _ -> "PixelYA16"
+  ImageRGB8 _ -> "PixelRGB8"
+  ImageRGB16 _ -> "PixelRGB16"
+  ImageRGBF _ -> "PixelRGBF"
+  ImageRGBA8 _ -> "PixelRGBA8"
+  ImageRGBA16 _ -> "PixelRGBA16"
+  ImageYCbCr8 _ -> "PixelYCbCr8"
+  ImageCMYK8 _ -> "PixelCMYK8"
+  ImageCMYK16 _ -> "PixelCMYK16"
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF tasty-discover #-}
