Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
Package: cardargus
Title: Generate SVG Information Cards with Embedded Fonts and Badges
Version: 0.2.4
Date: 2026-06-14
Version: 0.2.5
Date: 2026-06-29
Authors@R: c(
person("Andre", "Leite", email = "leite@castlab.org", role = c("aut", "cre")),
person("Hugo", "Vasconcelos", email = "hugo.vasconcelos@ufpe.br", role = "aut"),
person("Diogo", "Bezerra", email = "diogo.bezerra@ufpe.br", role = "aut"))
Description: Create self-contained SVG information cards with embedded 'Google
Fonts', shields-style badges, and custom logos. Cards are fully
portable SVG files ideal for dashboards, reports, and web applications.
Includes functions to export cards to PNG format and display them in
Includes functions to export cards to PNG and PDF formats and display them in
'R Markdown' and 'Quarto' documents.
License: MIT + file LICENSE
Encoding: UTF-8
Expand All @@ -22,8 +22,6 @@ Imports:
cli,
digest,
gdtools,
magick,
rsvg,
later
Suggests:
base64enc,
Expand All @@ -32,7 +30,9 @@ Suggests:
gfonts,
htmltools,
knitr,
magick,
rmarkdown,
rsvg,
showtext,
sysfonts,
systemfonts,
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export(save_svg)
export(setup_fonts)
export(svg_card)
export(svg_to_formats)
export(svg_to_pdf)
export(svg_to_pdf_chrome)
export(svg_to_png)
export(svg_to_png_chrome)
Expand Down
27 changes: 27 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,30 @@
# cardargus 0.2.5

## New features

* `svg_to_pdf()` exports a card to a **vector** PDF via `rsvg::rsvg_pdf()`,
mirroring `svg_to_png()` (sanitizes the SVG and embeds WOFF2 fonts). For
Chrome-based rendering, `svg_to_pdf_chrome()` remains available (#17).
* `save_card_for_knitr()` now accepts `format = "pdf"` (in addition to `"svg"`
and `"png"`), using Chrome when available and `svg_to_pdf()` otherwise.

## Minor improvements

* `svg_to_formats(formats = "pdf")` now reuses `svg_to_pdf()` for the vector path
and warns explicitly when it falls back to a rasterized PDF via `magick`.

## Bug fixes

* `magick` and `rsvg` moved from `Imports` to `Suggests`: they are only needed
for raster/PDF export, which is already guarded by `requireNamespace()`. This
lightens the install footprint and matches the package's "heavy deps in
Suggests" convention (#19).
* `svg_card()` now sizes badges using `value_fontsize` instead of a hard-coded
`10`, preventing clipped/misaligned badge text when `value_fontsize` differs
from 10 (#18).
* `is_light_color()` now reports an unknown color name with a clear `cli` error
instead of letting `col2rgb()` raise a raw base error (#20).

# cardargus 0.2.4

## CRAN policy fix
Expand Down
4 changes: 2 additions & 2 deletions R/cards.R
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ svg_card <- function(title = "FAR",
# First pass: calculate max height for uniform badges
max_badge_height <- 0
for (badge in badges_data) {
metrics_label <- gdtools::str_metrics(badge$label, fontname = font, fontsize = 10)
metrics_value <- gdtools::str_metrics(badge$value, fontname = font, fontsize = 10)
metrics_label <- gdtools::str_metrics(badge$label, fontname = font, fontsize = value_fontsize)
metrics_value <- gdtools::str_metrics(badge$value, fontname = font, fontsize = value_fontsize)
height_label <- as.numeric(metrics_label["ascent"]) + as.numeric(metrics_label["descent"])
height_value <- as.numeric(metrics_value["ascent"]) + as.numeric(metrics_value["descent"])
badge_h <- max(height_label, height_value) * 2
Expand Down
97 changes: 89 additions & 8 deletions R/conversion.R
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,13 @@ save_svg <- function(svg_content, output_path) {
#' @export
#'
#' @examples
#' svg <- svg_card("FAR", list(), list())
#' file_name <- tempfile(fileext = ".png")
#' png_path <- svg_to_png(svg, file_name, dpi = 300)
#' png_path <- svg_to_png(svg, file_name, dpi = 300, background = "white")
#' if (requireNamespace("rsvg", quietly = TRUE) ||
#' requireNamespace("magick", quietly = TRUE)) {
#' svg <- svg_card("FAR", list(), list())
#' file_name <- tempfile(fileext = ".png")
#' png_path <- svg_to_png(svg, file_name, dpi = 300)
#' png_path <- svg_to_png(svg, file_name, dpi = 300, background = "white")
#' }
svg_to_png <- function(svg_input,
output_path = NULL,
width = NULL,
Expand Down Expand Up @@ -257,6 +260,78 @@ svg_to_png <- function(svg_input,
cli::cli_abort("Failed to convert SVG to PNG (no working renderer found).")
}

#' Convert SVG to PDF
#'
#' @description
#' Convert an SVG string or SVG file path to a **vector** PDF. As with
#' [svg_to_png()], the SVG is sanitized and the required WOFF2 fonts are embedded
#' (downloaded on demand into a user cache) so text renders consistently.
#' Conversion uses \pkg{rsvg} ([rsvg::rsvg_pdf()]), preserving vector text.
#'
#' For headless-Chrome rendering (often best for web fonts), see
#' [svg_to_pdf_chrome()].
#'
#' @param svg_input SVG string or path to an SVG file.
#' @param output_path Output path for the PDF file (optional; a temp file is used if NULL).
#' @param width Output width in pixels (NULL to infer from the SVG).
#' @param height Output height in pixels (NULL to infer from the SVG).
#'
#' @return Path to the generated PDF file.
#' @export
#'
#' @examples
#' if (requireNamespace("rsvg", quietly = TRUE)) {
#' svg <- svg_card("FAR", list(), list())
#' file_name <- tempfile(fileext = ".pdf")
#' pdf_path <- svg_to_pdf(svg, file_name)
#' }
svg_to_pdf <- function(svg_input,
output_path = NULL,
width = NULL,
height = NULL) {

if (!requireNamespace("rsvg", quietly = TRUE)) {
cli::cli_abort(c(
"x" = "Vector PDF conversion requires the {.pkg rsvg} package.",
"i" = "Install with: {.code install.packages('rsvg')}",
"i" = "For Chrome-based PDF rendering, see {.fn svg_to_pdf_chrome}."
))
}

# Read SVG (file path vs string)
if (is.character(svg_input) && length(svg_input) == 1 && file.exists(svg_input)) {
svg_content <- paste(readLines(svg_input, warn = FALSE), collapse = "\n")
} else {
svg_content <- as.character(svg_input)
}

# Sanitize + embed fonts (download/cache if needed)
svg_content <- prepare_svg_for_raster(svg_content)

# Output path
if (is.null(output_path)) output_path <- tempfile(fileext = ".pdf")
ensure_output_dir(output_path)

# Infer dimensions from the SVG when not supplied (CSS px reference)
if (is.null(width)) {
w0 <- parse_svg_root_dim(svg_content, "width")
if (!is.na(w0)) width <- max(1L, round(w0))
}
if (is.null(height)) {
h0 <- parse_svg_root_dim(svg_content, "height")
if (!is.na(h0)) height <- max(1L, round(h0))
}

svg_file <- write_svg_tempfile(svg_content)

tryCatch(
rsvg::rsvg_pdf(svg_file, file = output_path, width = width, height = height),
error = function(e) cli::cli_abort("Failed to convert SVG to PDF: {e$message}")
)

output_path
}

#' Convert SVG to multiple formats
#'
#' @description
Expand Down Expand Up @@ -308,16 +383,22 @@ svg_to_formats <- function(svg_input,

} else if (identical(fmt, "pdf")) {
ensure_output_dir(output_path)

if (requireNamespace("rsvg", quietly = TRUE)) {
svg_file <- write_svg_tempfile(svg_prepared)
# Vector PDF (text preserved). Content is already prepared, but
# svg_to_pdf() re-runs prepare_svg_for_raster() idempotently.
tryCatch({
rsvg::rsvg_pdf(svg_file, file = output_path)
results$pdf <- output_path
results$pdf <- svg_to_pdf(svg_prepared, output_path)
}, error = function(e) {
cli::cli_warn("Failed to create PDF via rsvg: {e$message}")
})
} else if (requireNamespace("magick", quietly = TRUE)) {
# NOTE: magick rasterizes the SVG, so this produces a *raster* PDF
# (vector text is lost). Install {rsvg} for a vector PDF.
cli::cli_warn(c(
"!" = "Creating a rasterized PDF via {.pkg magick} (vector text lost).",
"i" = "Install {.pkg rsvg} for a vector PDF."
))
svg_file <- write_svg_tempfile(svg_prepared)
tryCatch({
img <- magick::image_read_svg(svg_file)
Expand Down
28 changes: 18 additions & 10 deletions R/knitr.R
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,15 @@ include_card_png <- function(svg_string,
#'
#' @param svg_string SVG string from `svg_card()`.
#' @param filename Output filename (without extension).
#' @param format Output format: `"svg"` or `"png"`.
#' @param format Output format: `"svg"`, `"png"`, or `"pdf"`.
#' @param dpi Rasterization DPI for PNG (default 300).
#' @param dir Output directory (defaults to knitr figure directory or tempdir()).
#' @param engine Rendering engine for PNG: `"auto"`, `"chrome"`, or `"rsvg"`.
#' @param engine Rendering engine for PNG/PDF: `"auto"`, `"chrome"`, or `"rsvg"`.
#' @return Path to the saved file.
#' @export
save_card_for_knitr <- function(svg_string,
filename = "card",
format = c("svg", "png"),
format = c("svg", "png", "pdf"),
dpi = 300,
dir = NULL,
engine = c("auto", "chrome", "rsvg")) {
Expand All @@ -160,9 +160,9 @@ save_card_for_knitr <- function(svg_string,
if (format == "svg") {
save_svg(svg_string, output_path)
} else {
# Determine which engine to use
# Determine which engine to use (relevant for png/pdf)
use_chrome <- FALSE

if (engine == "chrome") {
if (chrome_available()) {
use_chrome <- TRUE
Expand All @@ -173,14 +173,22 @@ save_card_for_knitr <- function(svg_string,
# Auto: prefer Chrome if available (better font rendering)
use_chrome <- chrome_available()
}

if (use_chrome) {
svg_to_png_chrome(svg_string, output_path, dpi = dpi, background = "transparent")

if (format == "pdf") {
if (use_chrome) {
svg_to_pdf_chrome(svg_string, output_path, background = "transparent")
} else {
svg_to_pdf(svg_string, output_path)
}
} else {
svg_to_png(svg_string, output_path, dpi = dpi, background = "transparent")
if (use_chrome) {
svg_to_png_chrome(svg_string, output_path, dpi = dpi, background = "transparent")
} else {
svg_to_png(svg_string, output_path, dpi = dpi, background = "transparent")
}
}
}

output_path
}

Expand Down
7 changes: 5 additions & 2 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ is_light_color <- function(color) {
cli::cli_abort("{.arg color} is not a valid hex color: {.val {color}}.")
}
} else {
# Try to get RGB from color name
rgb_vals <- col2rgb(color)
# Try to get RGB from color name (col2rgb() errors on unknown names)
rgb_vals <- tryCatch(col2rgb(color), error = function(e) NULL)
if (is.null(rgb_vals)) {
cli::cli_abort("{.arg color} is not a valid color: {.val {color}}.")
}
r <- rgb_vals[1]
g <- rgb_vals[2]
b <- rgb_vals[3]
Expand Down
2 changes: 1 addition & 1 deletion man/cardargus-package.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions man/save_card_for_knitr.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions man/svg_to_pdf.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions man/svg_to_png.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions tests/testthat/test-conversion.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,24 @@ test_that("detect_svg_fonts finds families and drops generics", {
expect_true("Montserrat" %in% fams)
expect_false("sans-serif" %in% fams)
})

test_that("svg_to_pdf writes a vector PDF", {
skip_if_not_installed("rsvg")
svg <- '<svg xmlns="http://www.w3.org/2000/svg" width="120" height="80"><rect width="120" height="80" fill="#4CAF50"/></svg>'
out <- tempfile(fileext = ".pdf")
res <- svg_to_pdf(svg, out)
expect_identical(res, out)
expect_true(file.exists(out))
expect_gt(file.info(out)$size, 0)
# PDF files start with the "%PDF" magic header
hdr <- readBin(out, "raw", n = 4)
expect_identical(rawToChar(hdr), "%PDF")
})

test_that("svg_to_pdf infers a temp output path when NULL", {
skip_if_not_installed("rsvg")
svg <- '<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><circle cx="25" cy="25" r="20"/></svg>'
out <- svg_to_pdf(svg)
expect_true(file.exists(out))
expect_match(out, "\\.pdf$")
})
Loading
Loading