BookmarkSubscribeRSS Feed

From SAS Visual Analytics to Pixel‑Perfect PDFs with a Local LLM (Part 2)

Started yesterday by
Modified yesterday by
Views 56

This application demonstrates how to build a sophisticated report generation pipeline that integrates with SAS Visual Analytics. It retrieves report objects (images and underlying data) from a SAS Viya environment and generates professional single-page PDF reports with customizable content—either a formatted data table or an AI-generated summary powered by a local Ollama model.

 

Articles in this series: 
From SAS Visual Analytics to Pixel‑Perfect PDFs with a Local LLM (Part 1): Meet the Tech Stack
From SAS Visual Analytics to Pixel‑Perfect PDFs with a Local LLM (Part 2): Understanding the Buildin... (this article)

 

The Big Picture

 

frame_01_xab_VA_ReportLab2-Flow.png

Select any image to see a larger version.
Mobile users: To view the images, select the "Full" version at the bottom of the page.

 

 

The Final Result

 

frame_02_xab_VA_Reportlab-result-2048x1579.png

 

 

Core Components

 

Step 3 - Authentication: session.py

Purpose: Establish a secure, authenticated HTTP session with SAS Viya.

 

Key Function: create_session(base_url, client_id, client_secret, username, password, cert_loc)

How it works:

 

  • Creates an httpx.Client configured with the SAS Viya base URL and SSL certificate location
  • Sends credentials to the /SASLogon/oauth/token endpoint to obtain a bearer token
  • Returns an authenticated client with the token pre-configured in the Authorization header
  • Uses Python's context manager pattern to ensure clean connection lifecycle

 

Example:

 

with create_session(...) as session:
    # session is now authenticated and ready for API calls
    response = session.get("/visualAnalytics/reports/...")

This context manager approach ensures that the HTTP connection is properly closed after use, preventing resource leaks.

 

 

Step 4 - Data Retrieval: va_report.py

Purpose: Interact with the SAS Visual Analytics API to extract report objects.

 

Key Functions:

 

direct_image_url_to_object_uri(image_url)

Converts a SAS "directImage" URL to a standardized object URI.

 

  • Input: A full directImage URL like:
    https://server.demo.sas.com/reportImages/directImage?reportUri=%2Freports%2Freports%2F<id>&visualElementName=ve22&...
  • Output: A normalized URI: /reports/reports/<report_id>/objects/<visual_element_name>

 

Why this matters: The directImage URL is what you get when sharing a report visualization in SAS Viya, but the VA API expects the normalized object URI format. This function bridges that gap by extracting the reportUri and visualElementName query parameters and restructuring them.

 

retrieve_image_from_report_object_uri(session, object_uri, image_format="png")

Fetches the rendered image of a report object.

 

  • Parses the object URI to extract the report ID and object name
  • Calls the VA API's rendering endpoint: /visualAnalytics/reports/{report_id}/png
  • Handles long-polling: The API may return 303 (redirect) while rendering. The function automatically follows redirects to get the final image.
  • Returns the image as binary bytes (PNG or SVG format)

 

retrieve_data_from_report_object_uri(session, object_uri, data_format="csv")

Fetches the underlying data table for a report object.

 

  • Similar to image retrieval, but targets the CSV/TSV/XLSX endpoint
  • Returns the data as binary content (typically CSV)

 

Key Insight: Both functions poll the same report object URI but request different representations—one visual (image), one tabular (data). This separation allows you to use both the visual and the raw data to build your report.

 

 

Step 9 - PDF Generation: pdf_report.py

Purpose: Create professional, single-page PDF reports with flexible layout and styling.

 

Architecture: The module uses a layered approach with private helpers and public APIs.

 

Layout Constants

The page is divided into fixed regions:

 

  • Page size: Landscape letter (11" × 8.5")
  • Margins: 36 points on all sides
  • Available width: Page width minus left and right margins
  • Content height: Total space for image + data/summary, minus title space

 

These constants ensure all content fits on one page with consistent spacing.

 

Private Helpers (Internal Logic)

_dataframe_to_table_rows(dataframe)

Converts a pandas DataFrame into a list of rows (header + data rows). This is the bridge between pandas data and ReportLab's table rendering.

 

_build_table(rows, available_width, font_size, cell_padding)

Creates a styled ReportLab Table from row data.

 

  • Distributes columns evenly across the available width
  • Applies header styling (bold background)
  • Adds alternating row colors (white and light blue) for readability
  • Adds grid lines and padding for a professional look

 

_fit_table_to_height(rows, available_width, max_height)

Intelligently shrinks a table to fit within a height constraint.

 

  • Tries multiple font sizes (9, 8, 7, 6) and padding combinations
  • If rows don't fit, shows only the top rows and appends "... N more rows"
  • Falls back to a minimal 2-row header layout if all else fails
  • Returns both the table and its actual rendered height

 

This function is crucial for ensuring the table never overflows the page while remaining readable.

 

_compose_summary_prompt_from_dataframe(dataframe, prompt_instructions, max_rows=15)

Builds a detailed prompt for the Ollama AI model.

 

  • Includes the user's prompt instructions (e.g., "write a business summary")
  • Appends dataset metadata: column names, row count
  • Includes a CSV preview of the first 15 rows
  • This gives the AI model context about the data structure and content

 

_fit_summary_paragraph(text, available_width, max_height)

Fits a text summary into the available space.

 

  • Renders the text as a ReportLab Paragraph with specified styling
  • If too tall, gradually truncates from the end
  • Returns both the paragraph and its rendered height

 

Public API

generate_table_from_dataframe(dataframe: pd.DataFrame) -> Table

Converts a DataFrame directly into a styled ReportLab Table optimized for the report's height constraint. Call this when you want to display structured data in table form.

 

generate_summary_paragraph_from_dataframe(dataframe, prompt_instructions, summary_model, ...) Generates an AI-powered text summary of the data.

 

  • Composes a detailed prompt from the DataFrame
  • Calls Ollama with the prompt and specified model
  • Fits the generated text into the available space
  • Returns a formatted Paragraph
  • Call this when you want an AI-generated narrative summary instead of numbers

 

create_single_page_pdf_report(title, image_path, bottom_content, output_path) The main public function that assembles and renders the PDF.

 

Rendering pipeline:

 

  1. Measures the bottom content (table or paragraph) to determine its height
  2. Calculates how much space remains for the image
  3. Loads and scales the image to fit the available space while maintaining aspect ratio
  4. Draws a left-side border in Viya colors (10 points wide, full page height)
  5. Renders the title in a colored header band (Viya surface background with dark accent bar)
  6. Draws the scaled image in the center
  7. Renders the bottom content (table or summary) at the bottom
  8. Writes the PDF to disk

 

Design principle: The layout is adaptive. If you provide a tall image, the bottom content shrinks. If you provide short content, the image gets more space. The function ensures everything always fits on one page.

 

Step 7 - Text Generation: text_generator.py

Purpose: Generate summaries and insights using a locally-running Ollama model.

 

Key Function: generate_text_with_ollama(prompt, model, ollama_base_url, timeout_seconds)

 

How it works:

 

  • Uses the ollama Python package to communicate with a local Ollama service (running at http://localhost:11434 by default)
  • Sends a prompt to the specified model (e.g., "qwen3")
  • Receives the generated text response
  • Handles timeouts and connection errors gracefully

 

Why Ollama?

 

  • Runs entirely locally—no data is sent to external services
  • No API keys or subscriptions required
  • Fast iteration for development
  • Models like Qwen are highly capable for business summarization tasks

 

Integration point: This function is called by generate_summary_paragraph_from_dataframe()to produce AI summaries of your data. The prompt and model choice are entirely controllable from main.py.

 

 

Orchestration: main.py

This is where everything comes together. The main() function demonstrates the complete workflow:

 

Setup Phase

content_mode = "summary"  # or "table"
summary_model = "qwen3"
summary_prompt_instructions = "You are writing a short business report summary..."

Configure these variables to control the report's content and tone.

 

Authentication Phase

with create_session(...credentials...) as session:

Establishes the authenticated connection to SAS Viya.

 

Data Retrieval Phase

object_link = "https://server.demo.sas.com/reportImages/directImage?..."
object_uri = direct_image_url_to_object_uri(object_link)

image = retrieve_image_from_report_object_uri(session, object_uri)
# Save to disk

data = retrieve_data_from_report_object_uri(session, object_uri)
# Convert to DataFrame
report_df = pd.read_csv(...)

Fetches both the visualization and the underlying data from SAS Visual Analytic's report.

 

Content Generation Phase

if content_mode == "summary":
    bottom_content = generate_summary_paragraph_from_dataframe(...)
else:
    bottom_content = generate_table_from_dataframe(report_df)

Chooses between a data table or an AI-generated summary based on configuration.

 

PDF Creation Phase

create_single_page_pdf_report(
    title="Xavier's Report Object Summary",
    image_path="output/report_object_image.png",
    bottom_content=bottom_content,
    output_path="output/report_object_summary.pdf"
)

Assembles the final PDF and writes it to disk.

 

 

Extending the Application

 

Creating Your Own Report

  1. Change the source object: Replace the object_link URL with a directImage URL from your SAS Viya environment. You can get this URL by right-clicking a visualization in SAS Visual Analytics and copying the share link.
  2. Customize the title: Update the title parameter in create_single_page_pdf_report() to reflect your report name.
  3. Switch content modes: Change content_mode from "summary" to "table" to toggle between AI summaries and raw data tables.
  4. Adjust the AI prompt: Modify summary_prompt_instructions to change the tone and focus of AI-generated summaries. For example:

 

    • "Provide a detailed technical analysis..." for technical audiences
    • "List the top 3 findings with business implications..." for executives

 

Modifying the PDF Layout

The pdf_report.py module is designed for easy customization:

 

  • Change colors: Update VIYA_PRIMARY, VIYA_PRIMARY_DARK, etc. at the top of the file
  • Adjust spacing: Modify MARGIN, SECTION_GAP, or MIN_IMAGE_HEIGHT constants
  • Alter title styling: Look in create_single_page_pdf_report() for the title rendering code
  • Change table styling: Edit the TableStyle configuration in _build_table()

 

Using a Different Ollama Model

 

Change the summary_model variable in main():

 

summary_model = "llama2"  # or any model you've pulled into Ollama

Ensure the model is already installed in your local Ollama instance:

 

ollama pull llama2

ollama run llama2  # Test that it works

 

 

Key Design Patterns

 

Context Managers for Resource Cleanup

The create_session() function uses Python's context manager protocol (with statement) to ensure HTTP connections are properly closed.

 

Adaptive Layout

The PDF layout is adaptive: images and tables dynamically size themselves to fit the available space without overflowing.

 

Lazy Imports

The Ollama module is imported only when needed, reducing startup time if not using AI features.

 

Separation of Concerns

Each module has a clear, single responsibility:

 

  • session.py → authentication
  • va_report.py → data retrieval
  • pdf_report.py → PDF rendering
  • text_generator.py → Ollama integration

 

This modularity makes testing and extending each component straightforward.

 

 

Summary

 

This application demonstrates a complete pipeline for intelligent report generation:

 

  1. Authenticate securely with SAS Viya
  2. Retrieve visual and tabular representations of report objects
  3. Transform the data into structured formats (DataFrames, prompts)
  4. Enrich with AI-generated insights (optional)
  5. Render professional, single-page PDFs with adaptive layouts
  6. Extend easily for custom use cases

 

By understanding these core components and their interactions, you can adapt this pipeline to generate reports for any dataset accessible through SAS Viya, and customize the styling, content, and tone to meet your specific requirements.

 

The code related to this article is available here.

 

Articles in this series: 
From SAS Visual Analytics to Pixel‑Perfect PDFs with a Local LLM (Part 1): Meet the Tech Stack
From SAS Visual Analytics to Pixel‑Perfect PDFs with a Local LLM (Part 2): Understanding the Buildin... (this article)

 

Find more articles from SAS Global Enablement and Learning here.

Contributors
Version history
Last update:
yesterday
Updated by:

Catch up on SAS Innovate 2026

Dive into keynotes, announcements and breakthroughs on demand.

Explore Now →

SAS AI and Machine Learning Courses

The rapid growth of AI technologies is driving an AI skills gap and demand for AI talent. Ready to grow your AI literacy? SAS offers free ways to get started for beginners, business leaders, and analytics professionals of all skill levels. Your future self will thank you.

Get started

Article Tags