diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Changelog for `hgg-pdf`
+
+## 0.1.0.0 — 2026-07-18
+
+First public release on Hackage.
+
+- PDF backend (HPDF).
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+BSD 3-Clause License
+
+Copyright (c) 2026, Toshiaki Honda
+All rights reserved.
+
+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/hgg-pdf.cabal b/hgg-pdf.cabal
new file mode 100644
--- /dev/null
+++ b/hgg-pdf.cabal
@@ -0,0 +1,50 @@
+cabal-version:      3.0
+name:               hgg-pdf
+version:            0.1.0.0
+extra-doc-files:    CHANGELOG.md
+synopsis:           PDF backend for hgg (HPDF)
+description:
+  Vector PDF backend for hgg. Interprets the [Primitive] list from
+  hgg-core into HPDF (Graphics.PDF, pure Haskell, BSD-3) drawing
+  commands, in the same single-pass architecture as the SVG backend.
+  Currently limited to the 14 standard PDF fonts (Latin only); for CJK
+  labels use hgg-rasterific (PNG + TrueType) instead.
+license:            BSD-3-Clause
+homepage:           https://github.com/frenzieddoll/hgg
+license-file:       LICENSE
+author:             Toshiaki Honda
+maintainer:         frenzieddoll@gmail.com
+copyright:          2026 Aelysce Project (Toshiaki Honda)
+category:           Graphics
+build-type:         Simple
+
+common warnings
+  ghc-options:        -Wall -Wcompat -Widentities -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns -Wpartial-fields
+                      -Wredundant-constraints
+
+library
+  import:           warnings
+  exposed-modules:  Graphics.Hgg.Backend.PDF
+  hs-source-dirs:   src
+  build-depends:    base               >= 4.17 && < 5
+                  , text               >= 2.0  && < 2.2
+                  , HPDF               >= 1.7  && < 1.9
+                  , hgg-core  ^>= 0.1
+                  , hgg-frame ^>= 0.1
+  default-language: Haskell2010
+
+test-suite hgg-pdf-tests
+  import:           warnings
+  type:             exitcode-stdio-1.0
+  main-is:          Spec.hs
+  hs-source-dirs:   test
+  default-language: Haskell2010
+  build-depends:    base
+                  , bytestring
+                  , directory
+                  , filepath
+                  , hspec              >= 2.10 && < 2.12
+                  , text
+                  , hgg-core
+                  , hgg-pdf
diff --git a/src/Graphics/Hgg/Backend/PDF.hs b/src/Graphics/Hgg/Backend/PDF.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Hgg/Backend/PDF.hs
@@ -0,0 +1,346 @@
+-- |
+-- Module      : Graphics.Hgg.Backend.PDF
+-- Description : ベクタ PDF backend (Phase 17、 HPDF)
+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)
+-- License     : BSD-3-Clause
+--
+-- plot-core の '[Primitive]' を HPDF (Graphics.PDF) の 'P.Draw' 命令に
+-- 解釈する (SVG backend と同じ Layer 1 構図)。 座標系は per-primitive で
+-- y 反転 (PDF = 左下原点 y 上向き / Primitive = SVG 系 左上原点 y 下向き。
+-- global flip だと text が鏡像になるため per-primitive 変換)。
+--
+-- ★v1 制約: フォントは PDF 標準 14 種 (Latin) のみ — 日本語ラベルは
+-- 出せない (非 Latin-1 文字は警告 + @?@ 置換)。 日本語の受け皿は
+-- hgg-rasterific (PNG + TrueType)。
+{-# LANGUAGE OverloadedStrings #-}
+module Graphics.Hgg.Backend.PDF
+  ( -- * 通常 (Resolver 不要 = inline 列のみの図)
+    savePDF
+    -- * Resolver 同伴 (= 'ColByName' を含む図)
+  , savePDFWith
+    -- * Phase 14 系: BoundPlot (df バインド済) を描画する
+  , savePDFBound
+    -- * 低レベル: [Primitive] を直接描画 (Phase 24 A8・3D backend glue 用)
+  , savePrimitivesPDF
+  ) where
+
+import           Graphics.Hgg.Frame    (BoundPlot (..))
+import           Graphics.Hgg.Layout   (Layout (..), Rect (..),
+                                        ViewportSize (..), computeLayout)
+import           Graphics.Hgg.Render   (FillStyle (..), LineStyle (..),
+                                        PathSegment (..), Point (..),
+                                        Primitive (..), StrokeStyle (..),
+                                        TextAnchor (..), TextStyle (..),
+                                        Transform (..), renderToPrimitives)
+import           Graphics.Hgg.Spec     (Resolver, VisualSpec, emptyResolver)
+import           Graphics.Hgg.Validate (Severity (..), diagnosticSeverity,
+                                        renderDiagnostic)
+import           Data.Char             (digitToInt, isHexDigit)
+import           Data.Complex          (Complex ((:+)))
+import           Data.Text             (Text)
+import qualified Data.Text             as T
+import qualified Graphics.PDF          as P
+import           System.IO             (hPutStrLn, stderr)
+
+-- ===========================================================================
+-- 入口 (SVG backend の saveSVG / saveSVGWith / saveSVGBound と対称)
+-- ===========================================================================
+
+-- | PDF ファイルに保存。 Resolver 不要 (= inline 列のみの図、 = 通常)。
+-- 列名参照を含む図は 'savePDFWith'、 DataFrame は 'savePDFBound' (@df |>> spec@)。
+savePDF :: FilePath -> VisualSpec -> IO ()
+savePDF path = savePDFWith path emptyResolver
+
+-- | 'Resolver' を渡して PDF ファイルに保存。 'ColByName' を含む図用。
+savePDFWith :: FilePath -> Resolver -> VisualSpec -> IO ()
+savePDFWith path r spec = do
+  -- ★ Phase 33 B5: PDF は point ネイティブ (PDFRect 単位 = pt) ゆえ k=1。layout/prims
+  --   は純 pt なので scalePrimitives 不要 (恒等)・viewport pt をそのまま頁サイズに。
+  --   raster backend のような dpi 乗算をするとサイズが二重変換になるので禁止。
+  let layout = computeLayout r spec
+      prims  = renderToPrimitives r layout spec
+      ViewportSize w h = lpViewport layout
+  savePrimitivesPDF path w h prims
+
+-- | Phase 24 A8: [Primitive] 列を所与のキャンバスサイズで PDF に直接描画する
+-- 低レベル経路 ('savePrimitivesSVG' の PDF 版)。 2D の 'savePDFWith' と 3D の
+-- 'savePDF3D' が共有。 非 Latin-1 ラベルは警告 + @?@ 置換 (v1 制約)。
+savePrimitivesPDF :: FilePath -> Int -> Int -> [Primitive] -> IO ()
+savePrimitivesPDF path w h prims = do
+  let (wD, hD) = (fromIntegral w, fromIntegral h)
+  warnNonLatin prims
+  fonts <- loadStdFonts
+  P.runPdf path P.standardDocInfo (P.PDFRect 0 0 wD hD) $ do
+    page <- P.addPage Nothing
+    P.drawWithPage page (drawPrims fonts hD prims)
+
+-- | v1 制約の loud 化: 非 Latin-1 文字を含むラベルがあれば stderr に 1 回警告
+-- (描画では @?@ に置換される)。 日本語ラベルは PNG backend
+-- (hgg-rasterific) を使う。
+warnNonLatin :: [Primitive] -> IO ()
+warnNonLatin prims
+  | any hasNonLatin prims =
+      hPutStrLn stderr
+        "[hgg-pdf] 警告: 非 Latin-1 文字を含むラベルがあります。 \
+        \PDF 標準フォントでは描けないため ? に置換されます (v1 制約)。 \
+        \日本語ラベルは PNG backend (hgg-rasterific) を使ってください。"
+  | otherwise = pure ()
+  where
+    hasNonLatin (PText _ s _) = T.any (> '\xFF') s
+    hasNonLatin _             = False
+
+-- | 'BoundPlot' (= @df |>> spec@ の結果) を PDF ファイルに保存。
+-- Error severity の検証診断は stderr に報告してから書き出す
+-- (saveSVGBound と同じ lenient 既定)。
+savePDFBound :: FilePath -> BoundPlot -> IO ()
+savePDFBound path (BoundPlot r spec diags) = do
+  mapM_ (hPutStrLn stderr . T.unpack . renderDiagnostic)
+        (filter ((== SevError) . diagnosticSeverity) diags)
+  savePDFWith path r spec
+
+-- ===========================================================================
+-- Primitive 解釈器
+-- ===========================================================================
+
+-- | Primitive 列を順に描く。 第 1 引数 = viewport 高さ (y 反転用)。
+--
+-- PClipPush/PTransformPush は対応する Pop までを**再帰グルーピング**して
+-- 'P.withNewContext' に入れる (PDF の clip / CTM は graphics state 復元で
+-- しか戻せないため。 SVG backend は \<g\> 開閉で済むのと対照的)。
+-- 対応の取れない Pop は黙って無視 (SVG backend と同じ寛容さ)。
+drawPrims :: StdFonts -> Double -> [Primitive] -> P.Draw ()
+drawPrims fonts h = go
+  where
+    go [] = pure ()
+    go (PClipPush rect : rest) =
+      let (inner, after) = breakMatch isClipPush isClipPop rest
+      in do P.withNewContext $ do
+              clipRectOf h rect
+              go inner
+            go after
+    go (PTransformPush tr : rest) =
+      let (inner, after) = breakMatch isTrPush isTrPop rest
+      in do P.withNewContext $ do
+              P.applyMatrix (matrixOf h tr)
+              go inner
+            go after
+    go (PClipPop : rest)      = go rest
+    go (PTransformPop : rest) = go rest
+    go (p : rest)             = drawOne fonts h p >> go rest
+
+    isClipPush p = case p of { PClipPush _ -> True; _ -> False }
+    isClipPop  p = case p of { PClipPop    -> True; _ -> False }
+    isTrPush   p = case p of { PTransformPush _ -> True; _ -> False }
+    isTrPop    p = case p of { PTransformPop    -> True; _ -> False }
+
+-- | 同種 push の入れ子を数えながら、 対応する pop までの内側と残りに割る。
+-- 対応 pop が無ければ全部内側 (= 末尾まで clip が効く・SVG の開きっ放しと同義)。
+breakMatch :: (Primitive -> Bool) -> (Primitive -> Bool)
+           -> [Primitive] -> ([Primitive], [Primitive])
+breakMatch isPush isPop = walk (0 :: Int)
+  where
+    walk _ [] = ([], [])
+    walk n (p : rest)
+      | isPop p && n == 0 = ([], rest)
+      | otherwise =
+          let n' = if isPush p then n + 1 else if isPop p then n - 1 else n
+              (inner, after) = walk n' rest
+          in (p : inner, after)
+
+-- | 単独 primitive の描画。 push/pop は 'drawPrims' が先に消費する。
+drawOne :: StdFonts -> Double -> Primitive -> P.Draw ()
+drawOne _ h (PLine a b ls)               = drawLine h a b ls
+drawOne _ h (PRect rect fs ms)           = drawRect h rect fs ms
+drawOne _ h (PCircle c rad fs ms _hover) = drawCircle h c rad fs ms
+drawOne _ h (PPath segs fs ms)           = drawPath h segs fs ms
+drawOne fonts h (PText p s ts)           = drawTextPrim fonts h p s ts
+drawOne _ _ _                            = pure ()
+
+-- | 矩形 clip: 矩形 path を current path にして 'P.setAsClipPath'。
+clipRectOf :: Double -> Rect -> P.Draw ()
+clipRectOf h (Rect x y w rh) = do
+  P.addShape (P.Rectangle (x :+ (h - y - rh)) ((x + w) :+ (h - y)))
+  P.setAsClipPath
+
+-- | SVG 系 Transform → PDF Matrix。 y 反転 F (y↦h−y) は per-primitive に
+-- 掛かるため、 SVG 空間の変換 M は **F∘M∘F (共役)** で PDF 空間に写す:
+-- translate (dx,dy) → translate (dx,−dy) / scale (sx,sy) →
+-- translate (0, h(1−sy)) ∘ scale (sx,sy)。 ※現状 core は PTransformPush を
+-- 発行しない (SVG backend も未対応) — 将来の発行に備えた整合実装。
+matrixOf :: Double -> Transform -> P.Matrix
+matrixOf _ (TranslateT dx dy) = P.translate (dx :+ negate dy)
+matrixOf h (ScaleT sx sy)     =
+  P.translate (0 :+ (h * (1 - sy))) * P.scale sx sy
+
+-- | パス: MoveTo/LineTo/CurveTo/ClosePath を HPDF path 命令に写して
+-- fill / stroke / 両方 を塗り分ける。
+drawPath :: Double -> [PathSegment] -> FillStyle -> Maybe StrokeStyle
+         -> P.Draw ()
+drawPath _ [] _ _ = pure ()
+drawPath h segs fs@(FillStyle fc _) ms = P.withNewContext $ do
+  applyFill fs
+  applyStroke ms
+  case segs of
+    (MoveTo p : rest) -> P.beginPath (pdfPt h p) >> mapM_ seg rest
+    _                 -> P.beginPath (0 :+ 0) >> mapM_ seg segs
+  paint
+  where
+    seg (MoveTo p)         = P.moveto (pdfPt h p)
+    seg (LineTo p)         = P.addLineToPath (pdfPt h p)
+    seg (CurveTo c1 c2 p)  = P.addBezierCubic (pdfPt h c1) (pdfPt h c2) (pdfPt h p)
+    seg ClosePath          = P.closePath
+    paint = case (fc == "none", ms) of
+      (True,  Just _)  -> P.strokePath
+      (True,  Nothing) -> pure ()
+      (False, Just _)  -> P.fillAndStrokePath
+      (False, Nothing) -> P.fillPath
+
+applyFill :: FillStyle -> P.Draw ()
+applyFill (FillStyle col opa)
+  | col == "none" = pure ()
+  | otherwise     = P.fillColor (colorOf col) >> P.setFillAlpha opa
+
+applyStroke :: Maybe StrokeStyle -> P.Draw ()
+applyStroke Nothing                    = pure ()
+applyStroke (Just (StrokeStyle sc sw)) =
+  P.strokeColor (colorOf sc) >> P.setWidth sw
+
+-- | SVG 系座標 (左上原点 y 下) → PDF 座標 (左下原点 y 上)。
+pdfPt :: Double -> Point -> P.Point
+pdfPt h (Point x y) = x :+ (h - y)
+
+-- | 線分: 色/幅/破線を設定して stroke。
+drawLine :: Double -> Point -> Point -> LineStyle -> P.Draw ()
+drawLine h a b (LineStyle col w dash) = P.withNewContext $ do
+  P.strokeColor (colorOf col)
+  P.setWidth w
+  setDashOf dash
+  P.beginPath (pdfPt h a)
+  P.addLineToPath (pdfPt h b)
+  P.strokePath
+
+-- | 矩形: fill (+ 任意 stroke)。 'Rect' は左上基準なので下端 = y + h 側。
+drawRect :: Double -> Rect -> FillStyle -> Maybe StrokeStyle -> P.Draw ()
+drawRect h (Rect x y w rh) fs ms = P.withNewContext $ do
+  let shape = P.Rectangle (x :+ (h - y - rh)) ((x + w) :+ (h - y))
+  fillShape shape fs ms
+
+-- | 円: fill (+ 任意 stroke)。 hover label は PDF では捨てる。
+drawCircle :: Double -> Point -> Double -> FillStyle -> Maybe StrokeStyle
+           -> P.Draw ()
+drawCircle h (Point cx cy) rad fs ms = P.withNewContext $ do
+  let shape = P.Circle cx (h - cy) rad
+  fillShape shape fs ms
+
+-- | Shape 共通: FillStyle (色 + opacity) で塗り、 StrokeStyle があれば縁取る。
+fillShape :: P.Shape a => a -> FillStyle -> Maybe StrokeStyle -> P.Draw ()
+fillShape shape (FillStyle col opa) ms = do
+  if col == "none"
+    then pure ()
+    else do
+      P.fillColor (colorOf col)
+      P.setFillAlpha opa
+      P.fill shape
+  case ms of
+    Nothing -> pure ()
+    Just (StrokeStyle sc sw) -> do
+      P.strokeColor (colorOf sc)
+      P.setWidth sw
+      P.stroke shape
+
+-- | LineStyle の dash 配列 (px) → HPDF DashPattern。 空 = 実線。
+setDashOf :: [Double] -> P.Draw ()
+setDashOf []   = P.setNoDash
+setDashOf dash = P.setDash (P.DashPattern dash 0)
+
+-- ===========================================================================
+-- テキスト (Phase 17 A3) — PDF 標準フォント (Latin のみ・v1 制約)
+-- ===========================================================================
+
+-- | 標準フォント束 (3 family × 4 変種)。 'mkStdFont' は AFM parse を伴う IO
+-- なので savePDF 入口で 1 回 load して 'P.Draw' 解釈器に渡す。
+data StdFonts = StdFonts
+  { sfSans  :: (P.AnyFont, P.AnyFont, P.AnyFont, P.AnyFont)
+    -- ^ Helvetica (regular, bold, oblique, bold-oblique)
+  , sfSerif :: (P.AnyFont, P.AnyFont, P.AnyFont, P.AnyFont)
+    -- ^ Times
+  , sfMono  :: (P.AnyFont, P.AnyFont, P.AnyFont, P.AnyFont)
+    -- ^ Courier
+  }
+
+loadStdFonts :: IO StdFonts
+loadStdFonts = do
+  let load fn = do
+        ef <- P.mkStdFont fn
+        case ef of
+          Right f  -> pure f
+          Left err -> errorWithoutStackTrace
+            ("hgg-pdf: 標準フォントの load に失敗: " ++ show err)
+      load4 (a, b, c, d) = (,,,) <$> load a <*> load b <*> load c <*> load d
+  StdFonts
+    <$> load4 (P.Helvetica, P.Helvetica_Bold,
+               P.Helvetica_Oblique, P.Helvetica_BoldOblique)
+    <*> load4 (P.Times_Roman, P.Times_Bold,
+               P.Times_Italic, P.Times_BoldItalic)
+    <*> load4 (P.Courier, P.Courier_Bold,
+               P.Courier_Oblique, P.Courier_BoldOblique)
+
+-- | family ("serif" / "monospace" 系は対応 family、 他は Helvetica) +
+-- weight/italic で 12 変種から選ぶ。
+selectFont :: StdFonts -> Text -> Text -> Bool -> P.AnyFont
+selectFont fonts fam weight italic =
+  let (r, b, o, bo)
+        | "serif" == fam || "Times" `T.isInfixOf` fam = sfSerif fonts
+        | "mono" `T.isInfixOf` fam || "Courier" `T.isInfixOf` fam = sfMono fonts
+        | otherwise = sfSans fonts
+  in case (weight == "bold", italic) of
+       (True,  True)  -> bo
+       (True,  False) -> b
+       (False, True)  -> o
+       (False, False) -> r
+
+-- | PText: anchor は 'P.textWidth' (AFM metrics) で x 補正、 tsRotate
+-- (degrees CW・SVG 同義) は (x,y) 周りの回転 = PDF (y 上向き) では符号反転。
+-- 非 Latin-1 文字は @?@ 置換 (警告は 'warnNonLatin' が IO 側で 1 回出す)。
+drawTextPrim :: StdFonts -> Double -> Point -> Text -> TextStyle -> P.Draw ()
+drawTextPrim fonts h (Point x y) txt ts = P.withNewContext $ do
+  let anyf  = selectFont fonts (tsFamily ts) (tsWeight ts) (tsItalic ts)
+      size  = max 1 (round (tsSize ts)) :: Int
+      font  = P.PDFFont anyf size
+      clean = T.map (\c -> if c > '\xFF' then '?' else c) txt
+      tw    = P.textWidth font clean
+      dx    = case tsAnchor ts of
+                AnchorStart  -> 0
+                AnchorMiddle -> negate (tw / 2)
+                AnchorEnd    -> negate tw
+  P.fillColor (colorOf (tsColor ts))
+  if tsRotate ts == 0
+    then P.drawText $ do
+           P.setFont font
+           P.textStart (x + dx) (h - y)
+           P.displayText clean
+    else do
+      P.applyMatrix (P.translate (x :+ (h - y)))
+      -- Phase 50 A1: 内部 tsRotate は CCW 正 (canonical)。 PDF/PostScript は y-up で CCW 正
+      --   ゆえ **恒等** で渡す (旧: CW canonical を negate していた。 canonical CCW 化で解消)。
+      P.applyMatrix (P.rotate (P.Degree (tsRotate ts)))
+      P.drawText $ do
+        P.setFont font
+        P.textStart dx 0
+        P.displayText clean
+
+-- ===========================================================================
+-- 色 (theme 色は全て "#rrggbb" hex。 named / 不正は黒 fallback)
+-- ===========================================================================
+
+colorOf :: Text -> P.Color
+colorOf t = case T.unpack t of
+  ['#', r1, r2, g1, g2, b1, b2]
+    | all isHexDigit [r1, r2, g1, g2, b1, b2] ->
+        P.Rgb (hex2 r1 r2) (hex2 g1 g2) (hex2 b1 b2)
+  ['#', r, g, b]
+    | all isHexDigit [r, g, b] -> P.Rgb (hex2 r r) (hex2 g g) (hex2 b b)
+  "white" -> P.Rgb 1 1 1
+  _       -> P.Rgb 0 0 0
+  where
+    hex2 a b = fromIntegral (digitToInt a * 16 + digitToInt b) / (255 :: Double)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,78 @@
+-- | hgg-pdf のテスト (Phase 17)。
+-- PDF バイト列はメタデータ等で非決定になり得るため golden 比較はせず、
+-- 「%PDF- 先頭 + 例外なし + サイズ下限」 を固定する (計画 md のリスク欄)。
+{-# LANGUAGE OverloadedStrings #-}
+module Main (main) where
+
+import           Graphics.Hgg.Backend.PDF (savePDF)
+import           Graphics.Hgg.Easy
+import qualified Data.ByteString.Char8    as BS
+import           System.Directory         (getTemporaryDirectory, removeFile)
+import           System.FilePath          ((</>))
+import           Test.Hspec
+
+main :: IO ()
+main = hspec $ do
+  describe "Phase 17 A1: savePDF (骨格 = line/rect/circle)" $ do
+    it "scatter spec が %PDF- 先頭の非自明なファイルを書く" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-scatter.pdf"
+          spec = layer (scatter (inline [1, 2, 3, 4]) (inline [2, 4, 1, 3]))
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      BS.length bs `shouldSatisfy` (> 500)
+      removeFile path
+
+    it "bar spec (rect 経路) も例外なく書ける" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-bar.pdf"
+          spec = layer (bar (inlineCat (["a", "b", "a"] :: [String]))
+                            (inline [1, 2, 3]))
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      removeFile path
+
+  describe "Phase 17 A2: PPath + clip 再帰グルーピング" $ do
+    it "density (PPath 経路) が例外なく書ける" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-density.pdf"
+          spec = layer (density (inline [1, 2, 2, 3, 3, 3, 4, 5]))
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      removeFile path
+
+    it "coordCartesianX (PClipPush/Pop 経路) が例外なく書ける" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-clip.pdf"
+          spec = layer (line (inline [1, 2, 3, 4]) (inline [2, 4, 1, 3]))
+                 <> coordCartesianX 1.5 3.5
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      removeFile path
+
+  describe "Phase 17 A3: PText (標準フォント・anchor/rotate)" $ do
+    it "title + 軸ラベル (回転 y ラベル含む) が例外なく書ける" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-text.pdf"
+          spec = layer (scatter (inline [1, 2, 3]) (inline [3, 1, 2]))
+                 <> title "Latin title"
+                 <> xLabel "weight" <> yLabel "mpg"
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      BS.length bs `shouldSatisfy` (> 1000)
+      removeFile path
+
+    it "非 Latin-1 ラベルでも crash しない (? 置換 + 警告)" $ do
+      tmp <- getTemporaryDirectory
+      let path = tmp </> "hgg-pdf-test-cjk.pdf"
+          spec = layer (scatter (inline [1, 2, 3]) (inline [3, 1, 2]))
+                 <> title "日本語タイトル"
+      savePDF path spec
+      bs <- BS.readFile path
+      BS.take 5 bs `shouldBe` "%PDF-"
+      removeFile path
