macOS ネイティブ技術だけで実装した EPUB 3 ツールキット。 日本語組版(縦組み・ルビ・縦中横・圏点・右綴じ)を第一級でサポートする。
An EPUB 3 toolkit built entirely with macOS-native technologies, with first-class support for Japanese typography: vertical writing, ruby, tate-chu-yoko, emphasis marks, and right binding.
macOS 14 以降・Swift 6。Xcode の File → Add Package Dependencies… に
https://github.com/shunnag/Washi.git を入力し、Up to Next Major Version: 1.20.0
で追加する。アプリのターゲットには、表示するなら Washi、解析・表紙・検索だけなら
WashiCore を選ぶ。通常の SwiftPM 利用で WashiDynamic を選ぶ必要はない。
Requires macOS 14+ and Swift 6. In Xcode, choose File → Add Package Dependencies…,
enter https://github.com/shunnag/Washi.git, and use Up to Next Major Version: 1.20.0.
Add Washi to your app target for rendering, or WashiCore for parsing, covers,
and search. Ordinary SwiftPM clients do not need WashiDynamic.
まず動作を見たい場合は、独立したサンプルを起動する。両方とも小さな EPUB を同梱し、 ファイル選択、ページ送り、検索とハイライト、読書位置の保存・復元を試せる。
To try a working app first, run either standalone sample. Both include a small EPUB and demonstrate file selection, paging, search and highlights, and position restoration.
git clone https://github.com/shunnag/Washi.git
cd Washi
Scripts/run-sample.sh AppKitReader
# SwiftUI 版 / SwiftUI version
Scripts/run-sample.sh SwiftUIReader- 導入と最初の表示 / Installation and first display
- サンプルの構成・実行方法 / Sample apps
- 公開ドキュメント / Online documentation
- SwiftUI への組み込み / SwiftUI integration
- ファイルアクセス / File access
- 検索・表紙・サムネイル / Search, covers, and thumbnails
- 読み込みと終了 / Loading and lifetime
公開ガイドとサンプルは main ブランチに追従する。利用 API は各ガイドに記載し、 現在のサンプルは Washi 1.20.0 の公開 API で動作する。
The online guides and samples follow the main branch. The current samples use the public API available in Washi 1.20.0.
Washi は macOS のシステムフレームワークだけで構成した、MIT ライセンスの
EPUB 3 ツールキット。第三者パッケージには依存しない。解析層は Foundation /
CoreFoundation / Compression / CryptoKit / CoreGraphics / ImageIO だけを使うため
ヘッドレスでも動作し、表示層は AppKit / WebKit を加える。縦組み(vertical-rl)・
ルビ・縦中横・圏点・右綴じを含む日本語組版を第一級の機能として扱う。
Washi is an MIT-licensed EPUB 3 toolkit built solely on macOS system
frameworks, with no third-party package dependencies. Its parsing layer uses
only Foundation / CoreFoundation / Compression / CryptoKit / CoreGraphics /
ImageIO and works headlessly; its rendering layer adds AppKit / WebKit.
Japanese typography is a first-class feature, including vertical writing
(vertical-rl), ruby, tate-chu-yoko, emphasis marks, and right binding.
-
依存ゼロ: ZIP 読み取り(zip64 対応・CRC 検証)から自前実装。 解析層は Foundation の
XMLDocument・CoreFoundation・Compression・CryptoKit・ CoreGraphics・ImageIO のみ(ヘッドレス利用可)、表示層(EPUBReaderView等)は AppKit・WebKit を使用Zero dependencies: implemented in-house, starting with ZIP reading (zip64 support and CRC validation). The parsing layer uses only Foundation's
XMLDocument, CoreFoundation, Compression, CryptoKit, CoreGraphics, and ImageIO (suitable for headless use); the rendering layer (EPUBReaderViewand related types) uses AppKit and WebKit. -
攻撃的 EPUB への耐性: zip 爆弾(比率+絶対上限)、XML 実体爆弾 (billion laughs。互換シムは許容)、異常な深さの XML、パス走査・ シンボリックリンク脱出をすべて入口で遮断(テスト付き)
Resilience against malicious EPUBs: blocks zip bombs (ratio and absolute limits), XML entity bombs (billion laughs, while allowing compatibility shims), excessively deep XML, path traversal, and symlink escapes at the point of entry, with tests.
-
EPUB 3.3 の RS(閲覧システム)要件に準拠する設計(EPUB 2.0.1 後方互換込み):
Designed to conform to EPUB 3.3 reading system (RS) requirements, with backward compatibility for EPUB 2.0.1:
-
OCF コンテナ(
container.xml複数 rootfile /mimetype検証 /encryption.xml)。.epubと展開済みフォルダの両方を開けるOCF containers (multiple rootfiles in
container.xml,mimetypevalidation, andencryption.xml). Opens both.epubfiles and unpacked directories. -
パッケージ文書: DCMES +
refines、display-seq、belongs-to-collection(シリーズ)、prefix宣言の正規化、rendition プロパティ、page-progression-direction、manifest フォールバック連鎖(循環ガード付き)、 EPUB 2 のopf:*属性・meta name="cover"Package documents: DCMES with
refines,display-seq,belongs-to-collection(series), normalization ofprefixdeclarations, rendition properties,page-progression-direction, manifest fallback chains with cycle guards, and EPUB 2opf:*attributes andmeta name="cover". -
ナビゲーション: EPUB 3 nav(toc / page-list / landmarks)+ NCX フォールバック
Navigation: EPUB 3 nav (toc / page-list / landmarks), with an NCX fallback.
-
本文抽出・全文検索: WebKit を使わず、大小文字・ダイアクリティカルマーク・ 全半角の区別を
EPUBSearchOptionsで個別指定できる。EPUBSearchHit.utf16Rangeは DOM Range と同じ UTF-16 コード単位Text extraction and full-text search without WebKit. Configure case, diacritic, and character-width sensitivity independently with
EPUBSearchOptions.EPUBSearchHit.utf16Rangeuses UTF-16 code units, just like DOM Range. -
XML 宣言と HTML の meta charset を解析・表示で共通判定し、Shift_JIS 系は NEC / IBM 拡張文字を含む CP932、EUC-JP は日本語 EUC として復号
Parsing and rendering share encoding detection for XML declarations and HTML meta charset. Shift_JIS variants are decoded as CP932, including NEC / IBM extensions, and EUC-JP is decoded as Japanese EUC.
-
フォント難読化の透過解除: IDPF(SHA-1/1040 バイト)と Adobe(UUID/1024 バイト)。DRM(ADEPT / LCP / FairPlay)は指紋検出して 明示的に報告(復号はしない)
Transparent font deobfuscation: IDPF (SHA-1 / 1040 bytes) and Adobe (UUID / 1024 bytes). DRM (ADEPT / LCP / FairPlay) is detected by its signatures and explicitly reported; it is not decrypted.
-
メディアオーバーレイ(SMIL)の再生:
playMediaOverlay()/pauseMediaOverlay()/stopMediaOverlay()で制御し、読み上げ箇所へmedia:active-classを付けてハイライトしながら必要なページへ自動追従Media overlay (SMIL) playback: control playback with
playMediaOverlay()/pauseMediaOverlay()/stopMediaOverlay(). The narrated passage is highlighted withmedia:active-class, and the reader automatically follows it to the appropriate page.
-
-
リフローレンダラー
EPUBReaderView(AppKit / WKWebView):Reflowable renderer
EPUBReaderView(AppKit / WKWebView):-
標準 CSS multicol によるページ分割。縦組みは「縦積みカラム + 無アニメーション ジャンプ」方式(Bibi / Readium CSS と同じ、実運用で実証済みのモデル。 行が途中で割れない)
Pagination with standard CSS multicol. Vertical writing uses vertically stacked columns and jumps without animation, the same proven model used by Bibi / Readium CSS, keeping lines from splitting midway.
-
Apple Books 風の版面: ウインドウ幅で単ページ⇔見開き 2 ページを 自動切替(
columnModeで固定も可)。縦書きの見開きは-webkit-column-axis: horizontalの半幅ページボックス(WKWebView 専用・ 実測検証済み)で右綴じの正順(先のページが右)。中央にノド、 各ページの下部中央に素のノンブル(showsPageFurnitureで OFF 可)。 表紙などの画像単独ページは見開き時も単独の中央フィットApple Books-style page layout: automatically switches between a single page and a two-page spread based on window width (or fixes the mode with
columnMode). Vertical spreads use half-width page boxes with-webkit-column-axis: horizontal(WKWebView-specific and verified by measurement), in right-bound reading order with the earlier page on the right. A center gutter separates the pages, and a plain folio (page number) is centered at the bottom of each page (disable withshowsPageFurniture). Single-image pages such as covers remain centered and fitted individually, even in spread mode. -
ライト/ダークテーマ: 既定でシステム外観に追従(
EPUBReaderThemeで 固定も可)。ダークは Apple Books 系のほぼ黒 + 明灰文字で、color-schemeも注入する。invertsGlyphImagesInDarkは小さなインライン 外字画像をヒューリスティックに反転し、無指定 fill のインライン SVG と 黒 stroke はcurrentColorで描画するLight and dark themes: follows the system appearance by default (or selects a fixed theme with
EPUBReaderTheme). The dark theme uses near-black backgrounds and light-gray text in the style of Apple Books, and injectscolor-scheme.invertsGlyphImagesInDarkheuristically inverts small inline glyph images; inline SVG with unspecified fill and black strokes is rendered withcurrentColor. -
電書連(DPFJ)EPUB 3 制作ガイド ver.1.1.4(2025-10、旧電書協 1.1.3 と CSS 互換)のテンプレートが使う抽象フォント名(
serif-ja等)を ヒラギノ明朝 ProN / ヒラギノ角ゴシックへ結び付ける@font-faceポリフィルAn
@font-facepolyfill maps abstract font names such asserif-jain the DPFJ EPUB 3 production guide templates (ver. 1.1.4, October 2025; CSS-compatible with the former EBPAJ 1.1.3 templates) to Hiragino Mincho ProN / Hiragino Kaku Gothic. -
WKURLSchemeHandlerによるコンテナ内配信(正しい MIME / CSP / Range 対応)。外部ネットワークはコンテンツルールで遮断、 本の JavaScript は既定で無効。有効化した場合も PAGE world の document-start script で WebRTC コンストラクタを使用不能にし、 CSP だけでは遮断できない STUN / UDP 経路を閉じるServes container resources through
WKURLSchemeHandlerwith correct MIME types, CSP, and Range support. Content rules block external network access, and the book's JavaScript is disabled by default. Even when enabled, a document-start script in the page world disables WebRTC constructors, closing STUN / UDP paths that CSP alone cannot block. -
EPUBReaderSettingsでフォント倍率・行間・横組み字間・段落間隔・ 著者フォントの上書き・ルビの表示/非表示、型付き配色・余白・ ユーザー CSS を指定。EPUBLocator(spine index + 進行率)で位置を保存/復元EPUBReaderSettingsconfigures font scale, line height, horizontal letter spacing, paragraph spacing, authored font overrides, ruby visibility, typed colors, insets, and user CSS. Save and restore reading positions withEPUBLocator(spine index + progression). -
宣言された
page-progression-direction、primary-writing-mode、冒頭の XHTML / CSS、RTL 言語の順でeffectiveReadingDirectionを決め、 表示層もその実効値を使用Determines
effectiveReadingDirectionfrom the declared page progression (page-progression-direction),primary-writing-mode, initial XHTML / CSS, and RTL language, in that order. The rendering layer uses this effective direction as well. -
内部リンク・目次・locator・UTF-16 範囲へのジャンプ元を最大 50 件保持する
canGoBack/goBack()と、履歴の利用可否が変わったときの delegate 通知canGoBack/goBack()retain up to 50 source positions for jumps via internal links, the table of contents, locators, and UTF-16 ranges, with delegate notifications when navigation history availability changes. -
EPUBInternalLinkとshouldFollowInternalLinkdelegate で内部リンクを 遷移前に判定。同一文書/別文書の脚注はnoteContent(for:)で抽出でき、hidesFootnoteAsidesで本文のページ割りから脚注 aside を除外できる。 抑止後にfollow(_:)を呼べば delegate を再度通さず、履歴を記録して遷移するInspect internal links before navigation with
EPUBInternalLinkand theshouldFollowInternalLinkdelegate. Extract footnotes in the same or another document withnoteContent(for:), and exclude footnote asides from body pagination withhidesFootnoteAsides. After intercepting a link, callfollow(_:)to navigate and record history without invoking the delegate again. -
正規化 UTF-16 範囲と reader-view 座標を結ぶ選択 API (
currentSelection/clearSelection()/rects(forTextRange:inSpineIndex:))と選択変更 delegateSelection APIs (
currentSelection/clearSelection()/rects(forTextRange:inSpineIndex:)) map normalized UTF-16 ranges to reader-view coordinates, with a delegate for selection changes. -
EPUB page-list のラベル一覧・移動・現在位置 (
printPageLabels/go(toPrintPage:)/currentPrintPage)に対応し、 本文の pagebreak marker とノンブル表示にも連動EPUB page-list support includes labels, navigation, and the current position (
printPageLabels/go(toPrintPage:)/currentPrintPage), integrated with pagebreak markers in the body and folio display. -
VoiceOver への確定ページ通知と accessibility label/value、システムの コントラスト増加・色以外での区別へ追従
Announces settled pages to VoiceOver, provides accessibility labels and values, and follows the system's Increase Contrast and Differentiate Without Color settings.
-
全文ページ数の census は、欠落または決定的に読み込めない spine 項目を 1 ページとして残りの計測を続け、部分的な結果も利用可能にする
The whole-book page-count census treats a missing spine item or one with a deterministic load failure as one page and continues measuring the rest, making partial results available.
-
ピンチでフォント倍率(0.5〜3.0 倍): ジェスチャ中は
WKWebView.magnificationで滑らかに視覚追従し、指を離すと倍率を確定して 進行率を保ったまま再ページ割り(テキストは再流し込みでシャープなまま)。adjustFontScale(by:)で段階調整も可、変更は delegate へ通知Pinch to adjust font scale (0.5–3.0×):
WKWebView.magnificationprovides smooth visual feedback during the gesture. On release, the scale is committed and the content is repaginated while preserving progression; reflow keeps the text sharp.adjustFontScale(by:)also supports incremental adjustments, with changes reported to the delegate. -
ホスト統合: キー/クリック/ファイルドロップの delegate 転送と、
EPUBContextMenuPolicy/ 表示直前 delegate によるコンテキストメニュー制御 (アプリ独自のキーバインドやページ送りに接続できる。既定では 左右端タップでページ送り)。キーはforwardsKeyEventsNativelyで ネイティブNSEventを横取り転送でき、WKWebView にキーを食われる 問題を避けられる(ホスト独自バインド向けの推奨経路)Host integration: forwards key, click, and file-drop events to delegates, and controls context menus with
EPUBContextMenuPolicyand a delegate called just before presentation. Connect these to app-specific key bindings or page turning; tapping the left or right edge turns pages by default.forwardsKeyEventsNativelyintercepts and forwards nativeNSEventkey events so WKWebView does not consume them, the recommended path for host-specific bindings.
-
-
固定レイアウト: viewport 解析、
page-spread-left/right/center、 「画像 1 枚だけのページ」の検出(WebKit を介さず画像を直接取り出せる — 日本の漫画 EPUB の大多数がこの形)、複雑ページの オフスクリーンラスタライズ(EPUBPageRasterizer)。device-width/device-heightの viewport はライブ表示の領域へ追従し、ラスタライズ時はdeviceViewportSizeで描画先寸法を渡せる。FixedLayoutPageInfo.viewportIsDeviceSizedで該当ページを判別できるFixed-layout: viewport parsing,
page-spread-left/right/center, detection of single-image pages (extract images directly without WebKit; most Japanese manga EPUBs use this form), and offscreen rasterization of complex pages (EPUBPageRasterizer). Adevice-width/device-heightviewport follows the live display area; for rasterization, pass the target dimensions withdeviceViewportSize. Identify these pages withFixedLayoutPageInfo.viewportIsDeviceSized. -
文書全体に加えて itemref ごとの
rendition:spread-*も文書順に解決し、 現在項目の表示と項目別 census の単ページ/見開き計画へ反映Resolves per-itemref
rendition:spread-*in document order in addition to publication-wide settings, and applies them to the current item's display and the single-page / spread plan for each spine item's census.
SwiftPM で依存に追加する:
Add Washi as a SwiftPM dependency:
// Package.swift
.package(url: "https://github.com/shunnag/Washi.git", from: "1.20.0")通常利用するプロダクトは 2 つ:
Two products cover typical use cases:
-
WashiCore— 解析層のみ(Foundation / CoreFoundation / Compression / CryptoKit / CoreGraphics / ImageIO)。AppKit/WebKit を引かないので、GUI セッションの ないヘッドレス利用(CLI・索引・サーバ・変換ツール)で使える。 OCF/OPF/nav 解析・メタデータ・本文抽出/検索・表紙デコードまで。WashiCore— the parsing layer only (Foundation / CoreFoundation / Compression / CryptoKit / CoreGraphics / ImageIO). It does not link AppKit or WebKit, so it supports headless use without a GUI session: CLI tools, indexing, servers, and converters. Includes OCF/OPF/nav parsing, metadata, text extraction and search, and cover decoding. -
Washi— 表示層込み(AppKit / WebKit を追加。リーダービュー・ ページ census・サムネイル)。WashiCoreを再輸出するので、import Washiだけで両層の公開 API が見える(従来どおり)。Washi— includes the rendering layer (adds AppKit / WebKit for the reader view, page census, and thumbnails). It re-exportsWashiCore, soimport Washiexposes the public APIs of both layers, as before.
// ヘッドレス: 解析・メタデータ・検索のみ
// Headless: parsing, metadata, and search only.
import WashiCore
let book = try EPUBPublication(url: url)
print(book.metadata.mainTitle ?? "", book.search("keyword").count)このほか、WashiDynamic は動的ライブラリとして 1 本にまとめたいホスト
(フレームワーク同梱など)向けで、両ターゲットを含む。
WashiDynamic includes both targets for hosts that want a single dynamic
library, for example when bundling a framework.
import Foundation
import Washi
// 解析(UI からは非同期の open を推奨。重い解析をメインで走らせない)
// Parse (prefer async open from UI code to keep heavy parsing off the main
// thread).
let publication = try await EPUBPublication.open(url: epubURL)
print(publication.metadata.mainTitle ?? "")
// 常に .ltr または .rtl
// Always .ltr or .rtl.
print(publication.effectiveReadingDirection)
// 判定に使った出典
// The source used to determine the direction.
print(publication.effectiveReadingDirectionSource)
for item in publication.navigation.toc { print(item.title) }
// 表示(AppKit)
// Display (AppKit).
let reader = EPUBReaderView()
reader.delegate = self
// at: EPUBLocator で位置復元
// Restore a position by passing an EPUBLocator to at:.
reader.load(publication: publication)
// 読書順で次ページ
// Next page in reading order.
reader.goForward()
// 物理方向(右綴じなら「進む」)
// Physical direction (forward for a right-bound book).
reader.turnPageLeft()
// 表紙(ライブラリ一覧用。宣言がない本もフォールバック連鎖で解決)
// Cover (for library listings; a fallback chain handles books without a
// cover declaration).
let cover = publication.coverImage(maxPixelSize: 480) // CGImage?
// 本文抽出・全文検索(WebKit 不要。索引・検索・引用に)
// Text extraction and full-text search (no WebKit; for indexing, searching,
// and quoting).
let plain = try publication.extractText(forSpineIndex: 0)
// 大小・全半角無視
// Case- and character-width-insensitive.
for hit in publication.search("吾輩") {
print(hit.spineIndex, hit.characterOffset, hit.snippet)
}
// 固定レイアウトの画像直取り
// Extract a fixed-layout image directly.
let info = try publication.fixedLayoutInfo(forSpineIndex: 0)
if let path = info.simpleImagePath {
// PNG/JPEG そのもの
// The original PNG/JPEG data.
let (data, _) = try publication.resource(at: path)
}ジャンプ履歴の利用可否は delegate で UI へ同期できる。通常のページ送りは この履歴に入らない。
Use the delegate to keep the UI in sync with navigation history availability. Normal page turns are not added to this history.
func readerViewNavigationHistoryDidChange(_ view: EPUBReaderView) {
print("戻る操作:", view.canGoBack ? "有効" : "無効")
}
if reader.canGoBack {
reader.goBack()
}noteref は既定遷移を止め、ホストのポップオーバーへ表示できる。
presentFootnote(_:anchor:) はホスト側の表示処理とする。
Intercept the default navigation for a noteref and show the note in a host
popover. presentFootnote(_:anchor:) represents the host's presentation code.
func readerView(
_ view: EPUBReaderView,
shouldFollowInternalLink link: EPUBInternalLink
) -> Bool {
guard link.isNoteReference else { return true }
Task { @MainActor in
if let note = await view.noteContent(for: link) {
presentFootnote(note, anchor: link.anchorRect)
}
}
return false
}
var footnoteSettings = reader.settings
footnoteSettings.hidesFootnoteAsides = true
reader.settings = footnoteSettings
// ポップオーバーの「本文で開く」操作などから呼ぶ
// Call from an action such as "Open in text" in the popover.
func openFootnoteInReader(_ link: EPUBInternalLink) {
reader.follow(link)
}文字組み設定はまとめて代入すると、1 回の再ページ割りで反映できる。
letterSpacingEm は CJK の縦組みには適用されない。
Assign typography settings together to apply them in a single repagination.
letterSpacingEm is not applied to vertical CJK text.
var typography = reader.settings
typography.lineHeightScale = 1.1
typography.letterSpacingEm = 0.03
typography.paragraphSpacingEm = 0.8
typography.fontFamilyOverride = "Hiragino Mincho ProN"
typography.hidesRuby = false
reader.settings = typography選択範囲は正規化済み UTF-16 オフセットと reader-view 座標で通知される。
Selections are reported as normalized UTF-16 offsets and reader-view coordinates.
func readerView(
_ view: EPUBReaderView,
selectionDidChange selection: EPUBTextSelection?
) {
guard let selection else { return }
print(selection.spineIndex, selection.text,
selection.utf16Range, selection.rects)
}census は load(publication:) の後に復元する。metricsKey には
EPUBScreenMetrics.paginationVersion が含まれ、古いページ割り方式の記録は
importCensus(_:) が自動的に拒否する。現在と異なる表示メトリクスの記録は、
同じ本・同じ世代なら受け入れられ、メトリクスが一致した時点で使われる。
Restore the census after load(publication:). The metricsKey includes
EPUBScreenMetrics.paginationVersion, so importCensus(_:) automatically
rejects records from older pagination algorithms. Records with different
display metrics are accepted for the same book and pagination version, and
are used once the metrics match.
reader.load(publication: publication)
if let savedRecord = try? JSONDecoder().decode(
EPUBCensusRecord.self, from: savedCensusData
) {
let accepted = reader.importCensus(savedRecord)
print("census 復元:", accepted)
}
if let currentRecord = reader.exportCensus() {
let dataToPersist = try JSONEncoder().encode(currentRecord)
// dataToPersist をホスト側で保存する
// Persist dataToPersist in the host.
}キーを扱うホスト側のデリゲートは、公開初期化子で生成した EPUBKeyEvent を
使って単体テストできる。key と code を指定し、shift / option /
control / command は省略すると false になる。
Host key-handling delegates can be unit-tested with an EPUBKeyEvent created
through its public initializer. Supply key and code; shift, option,
control, and command default to false.
import Washi
let event = EPUBKeyEvent(key: " ", code: "Space", shift: true)
hostDelegate.readerView(reader, didReceiveKey: event)
let consumed = hostDelegate.readerView(reader, shouldConsumeKey: event)hostDelegate は利用側が実装する EPUBReaderViewDelegate とする。
初期化子はイベントの値を生成し、上の例ではデリゲートを直接呼び出している。
hostDelegate is the host's implementation of EPUBReaderViewDelegate.
The initializer creates an event value; the example invokes the delegate directly.
主な EPUB 3.3 RS 要件の対応状況を以下に示す。✅ は対応済みを示す。
The table below summarizes support for selected EPUB 3.3 RS requirements. ✅ indicates support.
| 領域 | 状態 |
|---|---|
| OCF(ZIP / zip64 / mimetype / container.xml / encryption.xml) | ✅ |
| パッケージ文書(metadata refines / spine / rendition / fallback) | ✅ |
| ナビゲーション(nav の toc / landmarks、NCX フォールバック) | ✅ |
| EPUB page-list(一覧・移動・現在位置・本文 marker) | ✅ |
パッケージ metadata の dir / xml:lang |
✅(package / metadata から title・creator・contributor へ継承) |
実効読書方向(page-progression-direction / primary-writing-mode / CSS / 言語) |
✅ |
内部リンクと noteref(遷移前 delegate・脚注抽出) |
✅ |
| フォント難読化(IDPF / Adobe) | ✅ |
| リフロー描画(縦組み・ルビ・縦中横・圏点・右綴じ) | ✅ |
| 固定レイアウト(viewport / spread 指定 / SVG ラッパー) | ✅ |
| 本文テキスト抽出・全文検索(ルビ除去・大小/全半角無視) | ✅(解析層のみ) |
| メタデータ(著者/シリーズ/アクセシビリティの型付きサーフェス) | ✅ |
| scripted コンテンツ | 任意(既定オフ。CSP / 外部通信ルール / WebRTC 無効化込みで有効化可) |
| メディアオーバーレイ(SMIL) | パース+項目取得(mediaOverlay)。1.8.0 から playMediaOverlay() / pauseMediaOverlay() / stopMediaOverlay() で再生し、active-class ハイライトと自動ページ追従に対応 |
| DRM(ADEPT / LCP / FairPlay) | 非対応(検出して報告) |
| リフロー見開き(横組み / 縦組み) | ✅ |
| FXL 見開き合成 | 未実装(ホスト側で合成可) |
scripted コンテンツは任意で有効化できる(既定オフ)。メディアオーバーレイは 解析・項目取得に加え、1.8.0 から再生・ハイライト・自動ページ追従に対応する。 DRM は検出・報告のみで、FXL 見開き合成はホスト側で行う必要がある。
Scripted content can be enabled optionally and is off by default. Media overlays support parsing and clip retrieval, with playback, highlighting, and automatic page following since 1.8.0. DRM is only detected and reported; FXL spread composition must be handled by the host.
-
text-spacing-trimは WebKit に未実装のため、指定しても反映されない。text-spacing-trimhas no effect because WebKit does not implement it. -
hanging-punctuation: force-endは WebKit では効果がない。hanging-punctuation: force-endhas no effect in WebKit. -
EPUB 3.4 で outdated とされた機能のうち、
rendition:spread/rendition:flow/rendition:orientationは legacy hint として保持し、 フォント難読化・NCX・OPF 2 のmetaは互換性のため引き続き対応する。collection要素には未対応。Among the features marked outdated in EPUB 3.4,
rendition:spread,rendition:flow, andrendition:orientationare retained as legacy hints. Font obfuscation, NCX, and OPF 2metaremain supported for compatibility. Thecollectionelement is not supported. -
scrolled-docは章単位、scrolled-continuousは連続する章をつないで表示する。rollと旧pre-paginated+scrolled-continuousは、幅を合わせて隙間なく並べる。 スクロール中のページ番号は画面サイズに基づく区切りで、印刷ページ番号とは異なる。 詳細は スクロール表示 を参照。scrolled-docscrolls each chapter;scrolled-continuousjoins consecutive chapters. Roll and legacy fixed continuous content fit the viewport width without gaps. Screen numbers in these modes describe viewport-sized steps, not printed pages. -
text/html宣言の非準拠 spine は、ヘッドレスの本文抽出・検索では可能な範囲で 読むが、表示には XHTML 等の対応形式への fallback が必要。表示できない項目の 正確な検索位置・選択矩形・テキストアンカーは返さない。Nonconforming text/html spine items remain available for best-effort headless extraction/search, but rendering requires a supported fallback such as XHTML. Unrenderable items do not return exact text positions, range rectangles, or text anchors.
-
defersTapsForDoubleClick = trueは、ダブルクリックによる単語選択より先に ページ送りが起きるのを防ぐ代わりに、primary click の通知をシステムの ダブルクリック間隔だけ遅らせる。既定のfalseはクリックを即時通知する。defersTapsForDoubleClick = trueprevents a page turn from occurring before double-click word selection, but delays primary-click notifications by the system double-click interval. The default,false, reports clicks immediately. -
invertsGlyphImagesInDarkの外字判定はクラス名と表示寸法に基づくため、 小さな挿絵を外字と誤判定する場合がある。原色が必要な本ではfalseにする。invertsGlyphImagesInDarkidentifies glyph images by class names and rendered dimensions, so it may misclassify small illustrations. Set it tofalsefor books that need their original image colors. -
WebRTC コンストラクタの無効化は
allowsScriptedContent = trueで著者 JavaScript を許可した EPUB コンテンツだけが対象で、ホストアプリや別の WebView に対する一般的な WebRTC 制御ではない。WebRTC constructors are disabled only in EPUB content where authored JavaScript is allowed with
allowsScriptedContent = true. This is not a general WebRTC control for the host app or other WebViews.
-
公開コーパスのヘッドレススモークテストは、
WASHI_CORPUS_DIRに EPUB コーパスのディレクトリを指定してswift test --filter CorpusSmokeTestsを 実行する。未設定またはディレクトリが存在しない場合はスキップされる。To run headless smoke tests against a public corpus, set
WASHI_CORPUS_DIRto the EPUB corpus directory and runswift test --filter CorpusSmokeTests. The tests are skipped if the variable is unset or the directory does not exist. -
cooViewer の
Scripts/make-jp-epub-fixtures.pyで、日本語 EPUB の 合成フィクスチャを生成できる。このスクリプトは Washi には含まれない。 cooViewer リポジトリのルートで、第三者パッケージ不要の次のコマンドを実行する。cooViewer's
Scripts/make-jp-epub-fixtures.pygenerates synthetic Japanese EPUB fixtures. This script is not included in Washi. Run the following command from the cooViewer repository root; no third-party packages are required.python3 Scripts/make-jp-epub-fixtures.py <outdir> [--big]
macOS 14+ / Swift 6(strict concurrency)/ Apple Silicon・Intel 両対応の ソースだが、cooViewer 同梱ビルドは arm64 のみ。
The source supports macOS 14+, Swift 6 with strict concurrency, and both Apple Silicon and Intel. The build bundled with cooViewer is arm64-only.
-
消費者が保持するオフスクリーン型
EPUBScreenAtlas(census と サムネイルを内部に持つ)とEPUBPageRasterizerは、それぞれ不可視の NSWindow + WebContent プロセスを抱える。アトラス内部の census/サムネイルは 完了後 20 秒のアイドルで WebKit を自動解放し、次回要求で再構築するが、 使い終えたオフスクリーン型にはinvalidate()を呼ぶ(アトラスを キャッシュから追い出すときも)。EPUBReaderViewは ウインドウから外れた時点で自分のオフスクリーン(内部の census・ サムネイルレンダラ含む)を自動で畳むので、明示呼び出しは不要The offscreen types retained by the caller,
EPUBScreenAtlas(which holds the census and thumbnails) andEPUBPageRasterizer, each own an invisible NSWindow and WebContent processes. The atlas's internal census and thumbnail components automatically release WebKit after 20 seconds of idle time following completion and rebuild it on the next request. Even so, callinvalidate()when finished with an offscreen type, including when evicting an atlas from a cache.EPUBReaderViewautomatically tears down its offscreen resources, including its internal census and thumbnail renderers, when removed from a window; no explicit call is needed. -
オフスクリーン系 API は
.userInitiated以上の優先度で呼ぶこと。 低 QoS(.utility等)を継いだまま最初の JS 実行を発行すると、WebKit の 応答が返らず永久待ちになる(実測)Call offscreen APIs at
.userInitiatedpriority or higher. Issuing the first JavaScript execution while inheriting a low QoS such as.utilitycan leave WebKit unresponsive and cause an indefinite wait, as observed in testing. -
表示・計測系(Rendering/)は全て
@MainActor。GUI セッションのないデーモン からは解析層(EPUBPublicationほか)だけを使うAll rendering and measurement APIs (Rendering/) are
@MainActor. Daemons without a GUI session should use only the parsing layer (EPUBPublicationand related types). -
全文ページ数の実測(census)はオフスクリーン WebKit で数秒かかることが ある。
EPUBReaderView.exportCensus()の結果を保存し、再オープン時にimportCensus(_:)で注入すると再実測を省ける。同一版かつ現行のpaginationVersionの記録だけを受け入れ、メトリクスも一致すれば ページ番号/バーへ即時反映するMeasuring the whole-book page count (census) with offscreen WebKit can take several seconds. Persist the result of
EPUBReaderView.exportCensus()and inject it withimportCensus(_:)when reopening to skip remeasurement. Only records for the same edition and the currentpaginationVersionare accepted. If the metrics also match, the page number and page bar update immediately.
公開 DocC / Online DocC から Washi と WashiCore の両方を参照できる。main 更新時にガイドのコード例とサンプルを検証して公開する。
Browse both modules in the online DocC documentation. Guides and samples are checked before documentation is published from main.
公開 API の doc コメントと DocC カタログ記事は、日本語を正(ベース)として 英語を併記する方針で、後続の文書整備で順次対応する。DocC では両者を合わせて ドキュメントを生成できる。
Public API doc comments and DocC catalog articles will use Japanese as the authoritative base, with English alongside it. This policy will be applied in subsequent documentation updates. DocC can generate documentation from both the comments and the articles.
Swift Package Index 用の設定(.spi.yml)では、Washi / WashiCore の両ターゲットを
ドキュメント生成対象に指定している。公開時の
パッケージ登録もここから行える。
The Swift Package Index configuration (.spi.yml) enables documentation
generation for both Washi and WashiCore. When publishing, use the
package registration page
to register the package.
Washi パッケージのルートで、ローカルの DocC を次のコマンドでビルドできる。
Build DocC documentation locally from the Washi package root with:
xcodebuild docbuild -scheme Washi -destination 'platform=macOS'ガイド内の全 Swift コード例の型検査と静的サイトの生成も実行できる。出力先には
空のディレクトリを指定する。サンプルの実行検証は swift test --package-path Samples。
To typecheck all Swift examples in the guides and generate the static site, use an
empty output directory. Run sample integration tests with swift test --package-path Samples.
python3 Scripts/build-documentation.py --output-dir .build/docs-site --derived-data .build/docs-derivedこのリポジトリが Washi の正リポジトリであり、開発もここで行う。 Issue / PR はこのリポジトリで受け付ける。 cooViewer は、このパッケージの利用者のひとつ。
This is Washi's canonical repository, where development takes place and issues and pull requests are accepted. cooViewer is one of the applications that uses this package.
開発課題はこのリポジトリの beads で管理する。初回は bd bootstrap --yes、
作業の確認は bd ready を使う。cooViewer から移した課題は旧 ID を維持している。
詳しい手順は .beads/README.md を参照する。
Development tasks are tracked in this repository's Beads database. Run
bd bootstrap --yes on a new checkout and bd ready to find available work.
Issues migrated from cooViewer retain their original IDs.
公開予定の版を CHANGELOG に ## [X.Y.Z] - YYYY-MM-DD と空でない本文で記録し、
EPUBReadingSystem.version も同じ版に更新する。変更をコミットしてから次を実行する。
作業ツリー(未追跡ファイルを含む)がクリーンで、
公開先の最新確定版タグより新しい版であることも検証する。
Record the planned version in CHANGELOG as ## [X.Y.Z] - YYYY-MM-DD with nonempty
release notes, and set EPUBReadingSystem.version to that version. Commit the changes
and run the command below. It also requires a clean working tree,
including untracked files, and a version newer than the latest stable tag on
the public remote.
Scripts/release.sh X.Y.Z
# 公開先を切り替える場合
Scripts/release.sh X.Y.Z --remote originこのスクリプトは検証のみを行う。対象コミットの CI が成功したことを確認してから、
タグの作成・公開を別途行う。Python 3.9 以降と Git が必要で、cooViewer には依存しない。
スクリプトの検証は python3 -m unittest discover -s Tests/Scripts -p 'test_*.py' で実行する。
The script performs validation only. Create and publish the tag separately, after the target commit's CI has passed. It requires Python 3.9 or later and Git, and works independently of cooViewer.
タグを push すると、テスト・配布構成ビルド・公開 EPUB コーパス検証がすべて
成功した後、CI がそのタグの CHANGELOG から GitHub Release を自動作成する。
確定版タグ (X.Y.Z または vX.Y.Z) と日付付きの変更履歴が必要。
タグは CI 完了前にも SwiftPM から利用できるため、タグ作成前の確認も必要になる。
After a tag is pushed, CI creates its GitHub Release from the tagged CHANGELOG
only after all tests, release artifact builds, and public EPUB corpus checks pass.
This requires a stable tag (X.Y.Z or vX.Y.Z) and a dated changelog entry.
SwiftPM can resolve the tag before CI finishes, so the pre-tag check still matters.
公開処理だけが失敗した場合は、そのタグの CI で失敗したジョブを再実行する。
公開済みの Release は上書きせず、同じタグの下書きがある場合は自動公開を止める。
下書きの内容を確認して手動公開するか、下書きを整理してから再実行する。
旧タグの公開漏れを手動で補う場合も、ノートを確認した上で
gh release create X.Y.Z --verify-tag --notes-file notes.md --latest=false を使う。
自動公開では公開先の最新確定版タグだけを Latest にし、過去の版の再実行による
巻き戻りを防ぐ。GitHub CLI (gh) が必要で、CI の認証には GITHUB_TOKEN を使う。
If only publication fails, rerun the failed jobs in that tag's CI run. Published
Releases are preserved; an existing draft stops automatic publication so its
content can be reviewed and published manually, or the draft resolved before retrying.
For historical tags, review the notes and use
gh release create X.Y.Z --verify-tag --notes-file notes.md --latest=false.
Automatic publication marks only the newest stable remote tag as Latest, so
retries for older versions cannot move it backwards. The publisher uses GitHub
CLI (gh) and authenticates in CI with GITHUB_TOKEN.
MIT License(LICENSE を参照)。依存パッケージはない。 設計にあたり Readium CSS・Bibi(いずれも実装は独立)の公開知見を参考にした。
MIT License (see LICENSE). There are no package dependencies. The design draws on publicly shared findings from Readium CSS and Bibi; Washi's implementation is independent of both.