Date: 2025-02-24
Status
Accepted
Context
As part of our Elixir replatforming effort for the ebook conversion system, we need to provide effective preview capabilities for PDF output. PDF previewing is a critical component of our workflow for several reasons:
- Quality assurance: Team members need to visually inspect PDF outputs to verify proper formatting, layout, and typesetting
- Rapid iteration: The editing and conversion process requires quick feedback loops to address formatting issues
- Annotation needs: Content reviewers need to add comments and annotations directly to PDFs
- Cross-reference validation: Visual inspection helps confirm proper functioning of cross-references, indices, and bibliographies
- External tool integration: Our system needs to launch external viewers and potentially capture feedback
Our replatforming strategy emphasizes improved build environment management and quality control, both of which depend on reliable PDF preview capabilities.
The PDF viewer needs to satisfy several requirements:
- Integration with macOS (our primary development platform)
- Programmatic control from Elixir
- Support for annotations
- Fast rendering of complex technical documents
- Support for refresh/reload when PDFs are regenerated
- Customizable interface for different workflows
- Multi-document comparison capabilities
Decision
We will use Skim (https://skim-app.sourceforge.io/) as our primary PDF viewer for the ELib platform, integrating it with our Elixir-based ebook conversion system.
Implementation Details
-
Integration Method:
We will use Elixir'sSystem.cmd/3andPortmodules to integrate with Skim:defmodule Codexis.Viewers.PDF do def preview(pdf_path) do System.cmd("open", ["-a", "Skim", pdf_path]) end def preview_with_options(pdf_path, options) do args = build_skim_args(pdf_path, options) Port.open({:spawn_executable, skim_executable()}, args) end defp skim_executable do "/Applications/Skim.app/Contents/MacOS/Skim" end defp build_skim_args(pdf_path, options) do base_args = [{:args, [pdf_path]}] # Add additional args based on options Enum.reduce(options, base_args, fn {key, value}, acc -> add_option(acc, key, value) end) end # Implementation for various options... end -
Advanced Features:
defmodule Codexis.Viewers.PDF.Skim do # Refresh an already open PDF def refresh(pdf_path) do script = """ tell application "Skim" set theDocs to get documents whose path is "#{pdf_path}" if length of theDocs > 0 then set theDoc to item 1 of theDocs revert theDoc end if end tell """ System.cmd("osascript", ["-e", script]) end # Open PDFs in comparison mode def compare(pdf_path1, pdf_path2) do script = """ tell application "Skim" open "#{pdf_path1}" open "#{pdf_path2}" tell application "System Events" tell process "Skim" click menu item "Tile" of menu "Window" of menu bar 1 end tell end tell end tell """ System.cmd("osascript", ["-e", script]) end # Get annotations from a PDF def get_annotations(pdf_path) do # Implementation to extract annotations using Skim's export functionality end end -
Integration with Build Pipeline:
defmodule Codexis.Build.Formats.PDF do def generate_and_preview(content, options) do with {:ok, pdf_path} <- generate_pdf(content, options), {:ok, _} <- Codexis.Viewers.PDF.preview(pdf_path) do {:ok, pdf_path} end end def auto_refresh_preview do # Set up a file system watcher to auto-refresh Skim when PDFs change fs_path = Path.join([build_dir(), "*.pdf"]) {:ok, watcher_pid} = FileSystem.start_link(dirs: [fs_path]) FileSystem.subscribe(watcher_pid) # Handle file change events receive do {:file_event, _watcher_pid, {path, _events}} -> if Path.extname(path) == ".pdf" do Codexis.Viewers.PDF.Skim.refresh(path) end end end end -
Configuration Options:
# In configuration config :elib, Codexis.Viewers.PDF, default_viewer: :skim, skim_path: "/Applications/Skim.app/Contents/MacOS/Skim", presentation_mode: false, auto_refresh: true, annotation_export_path: "annotations"
Consequences
Positive
- Enhanced productivity: Skim's quick rendering and navigation features speed up the review process
- Improved annotation workflow: Skim's robust annotation tools enable better feedback capture
- macOS integration: Native macOS app with support for AppleScript automation
- Lightweight: Faster startup time compared to alternatives like Adobe Acrobat
- Academic-focused features: Well-suited for technical documents with math content
- Auto-reload capability: Supports refreshing when PDFs are regenerated
- Multi-document support: Enables comparison between different versions or formats
- Free and open-source: No licensing costs or restrictions
Negative
- Platform limitation: Skim is macOS-only, limiting use on other operating systems
- AppleScript dependency: Automation relies on AppleScript, which may change over time
- Limited enterprise features: Lacks some advanced features found in commercial alternatives
- Integration complexity: Requires more custom code compared to using a web-based viewer
- External dependency: Relies on externally maintained software not controlled by our team
Mitigation Strategies
-
Cross-platform fallback: Implement a fallback PDF viewing strategy for non-macOS platforms:
def preview(pdf_path) do case :os.type() do {:unix, :darwin} -> open_with_skim(pdf_path) {:unix, _} -> System.cmd("xdg-open", [pdf_path]) {:win32, _} -> System.cmd("start", ["", pdf_path], [shell: true]) end end -
Web fallback option: Implement a browser-based PDF preview as an alternative:
defmodule Codexis.Viewers.PDF.Web do def start_server(pdf_dir) do # Start a small web server to serve PDFs with PDF.js end end -
Viewer abstraction layer: Create an abstraction that allows for different viewers:
defmodule Codexis.Viewers.PDF do def preview(pdf_path) do viewer = Application.get_env(:elib, :pdf_viewer, :skim) preview_with(viewer, pdf_path) end defp preview_with(:skim, path), do: Codexis.Viewers.PDF.Skim.preview(path) defp preview_with(:web, path), do: Codexis.Viewers.PDF.Web.preview(path) defp preview_with(:system, path), do: Codexis.Viewers.PDF.System.preview(path) end -
Sandbox annotations: Create a controlled annotation export/import system less dependent on specific viewer features
-
Regular testing: Verify Skim compatibility with each macOS update
References
- Skim official website: https://skim-app.sourceforge.io/
- macOS AppleScript documentation for application automation
- Elixir System and Port module documentation
- PDF.js documentation (for web fallback option)