Nothing
# This file contains basic functions for generating valid GIFTI XML documents and writing them to files.
#
# Note that GIFTI is a very versatile and complex format, and one can write a lot of different things in different formats.
# While this is great, it also means that it is not guaranteed that a software x will be able to interprete the data stored in a GIFTI file by software y in the
# way intended by y (even though the file is technically a valid GIFTI file).
# The functions in here leave the user the ability to edit the generated XML trees (e.g., with the xml2 package), so you should be able to use these functions
# to write data exporters for any software.
#
# Some writers for how a specific software stores data of certain typpes (surface, measurements/morphometry, labels) in GIFTI format files are implemented in separate
# source files. E.g., writers for FreeSurfer can be found in gifti_writer_freesurfer.R.
#' @title Get GIFTI XML representation of data.
#'
#' @description Creates a GIFTI XML tree from your datasets (vectors and matrices). The tree can be further modified to add additional data, or written to a file as is to produce a valid GIFTI file (see \code{\link{gifti_xml_write}}).
#'
#' @param data_array list of data vectors and/or data matrices.
#'
#' @param intent vector of NIFTI intent strings for the data vectors in 'data_array' parameter, see \code{\link[gifti]{convert_intent}}. Example: 'NIFTI_INTENT_SHAPE'. See https://nifti.nimh.nih.gov/nifti-1/documentation/nifti1fields/nifti1fields_pages/group__NIFTI1__INTENT__CODES.html.
#'
#' @param datatype vector of NIFTI datatype strings. Example: 'NIFTI_TYPE_FLOAT32'. Should be suitable for your data.
#'
#' @param encoding vector of encoding definition strings. One of 'ASCII', 'Base64Binary', 'GZipBase64Binary'.
#'
#' @param endian vector of endian definition strings. One of 'LittleEndian' or 'BigEndian'. See \code{\link[gifti]{convert_endian}}.
#'
#' @param transform_matrix optional, a list of transformation matrices, one for each data_array. If one of the data arrays has none, pass `NA`. Each transformation matrix in the outer list has to be a 4x4 matrix or given as a named list with entries 'transform_matrix', 'data_space', and 'transformed_space'. Here is an example: \code{list('transform_matrix'=diag(4), 'data_space'='NIFTI_XFORM_UNKNOWN', 'transformed_space'='NIFTI_XFORM_UNKNOWN')}.
#'
#' @param force logical, whether to force writing the data, even if issues like a mismatch of datatype and data values are detected.
#'
#' @return xml tree, see xml2 package. One could modify this tree as needed using xml2 functions, e.g., add metadata.
#'
#' @references See https://www.nitrc.org/frs/download.php/2871/GIFTI_Surface_Format.pdf
#'
#' @note Unless you want to modify the returned tree manually, you should not need to call this function. Use \code{\link{gifti_writer}} instead.
#'
#' @seealso The example for \code{\link{gifti_xml_write}} shows how to modify the tree.
#'
#' @examples
#' \dontrun{
#' my_data_sets <- list(rep(3.1, 3L), matrix(seq(6) + 0.1, nrow = 2L))
#' transforms <- list(NA, list(
#' "transform_matrix" = diag(4), "data_space" = "NIFTI_XFORM_UNKNOWN",
#' "transformed_space" = "NIFTI_XFORM_UNKNOWN"
#' ))
#' xmltree <- gifti_xml(my_data_sets, datatype = "NIFTI_TYPE_FLOAT32", transform_matrix = transforms)
#' # Verify that the tree is a valid GIFTI file:
#' gifti_xsd <- "https://www.nitrc.org/frs/download.php/158/gifti.xsd"
#' xml2::xml_validate(xmltree, xml2::read_xml(gifti_xsd))
#' }
#'
#' @importFrom xml2 xml_new_root xml_set_attr xml_add_child read_xml
#' @export
gifti_xml <- function(data_array, intent = "NIFTI_INTENT_SHAPE", datatype = "NIFTI_TYPE_FLOAT32", encoding = "GZipBase64Binary", endian = "LittleEndian", transform_matrix = NULL, force = FALSE) {
if (requireNamespace("gifti", quietly = TRUE)) {
if (!is.list(data_array)) {
stop("Parameter 'data_array' must be a list.")
}
supported_encodings <- c("ASCII", "Base64Binary", "GZipBase64Binary")
num_data_arrays <- length(data_array)
dataarray_contains_matrices <- any(lapply((lapply(data_array, dim)), length) > 0L)
num_transform_matrices <- 0L
if (!is.null(transform_matrix)) {
if (!is.list(transform_matrix)) {
stop("Parameter 'transform_matrix' must be NULL or a list.")
}
if (length(names(transform_matrix)) != 0L) {
stop("Parameter 'transform_matrix' must not be a named list. Hint: When passing a single matrix, you need to enclose it in an outer list.")
}
num_transform_matrices <- length(transform_matrix)
if (num_transform_matrices != num_data_arrays) {
stop(sprintf("Found %d data arrays, but %d transform matrices: mismatch. Pass NA if you have no matrix for a data array.\n", num_data_arrays, num_transform_matrices))
}
}
if (num_data_arrays > 1L) {
if (length(intent) == 1L) {
intent <- rep(intent, num_data_arrays)
}
if (length(datatype) == 1L) {
datatype <- rep(datatype, num_data_arrays)
}
if (length(encoding) == 1L) {
encoding <- rep(encoding, num_data_arrays)
}
if (length(endian) == 1L) {
endian <- rep(endian, num_data_arrays)
}
}
dim0 <- rep(1L, num_data_arrays) # gets filled later
dim1 <- rep(1L, num_data_arrays) # gets filled later
dimensionality <- rep(1L, num_data_arrays) # gets filled later
array_indexing_order <- rep("RowMajorOrder", num_data_arrays) # currently fixed
root <- xml2::xml_new_root("GIFTI", "Version" = "1.0", "NumberOfDataArrays" = num_data_arrays)
metadata <- xml2::xml_add_child(root, read_xml("<MetaData></MetaData>"))
xml2::xml_add_child(metadata, read_xml("<MD><Name>Generator</Name><Value>fsbrain</Value></MD>"))
da_index <- 1L
data_is_matrix <- FALSE
for (da in data_array) {
if (is.vector(da)) {
dim0[da_index] <- length(da)
dim1[da_index] <- 1L
dimensionality[da_index] <- 1L
} else if (is.matrix(da)) {
data_is_matrix <- TRUE
dimensionality[da_index] <- 2L
dim0[da_index] <- dim(da)[1]
dim1[da_index] <- dim(da)[2]
if (array_indexing_order[da_index] == "RowMajorOrder") {
da <- as.vector(t(da))
} else if (array_indexing_order[da_index] == "ColumnMajorOrder") {
da <- as.vector((da))
} else {
stop(sprintf("Dataarray # %d: invalid array_indexing_order, must be 'RowMajorOrder' or 'ColumnMajorOrder'.\n", da_index))
}
} else {
stop("The data_arrays must be of type vector or matrix.")
}
if (!encoding[da_index] %in% supported_encodings) {
stop(sprintf("Dataarray # %d: invalid encoding '%s'.\n", da_index, encoding[da_index]))
}
if (!endian[da_index] %in% c("LittleEndian", "BigEndian")) {
stop(sprintf("Dataarray # %d: invalid endian '%s'.\n", da_index, endian[da_index]))
}
check_data_and_settings_consistency(da_index, da, datatype[da_index], intent[da_index], force = force)
data_array_node <- xml2::read_xml("<DataArray/>")
xml2::xml_set_attr(data_array_node, "Dimensionality", dimensionality[da_index])
xml2::xml_set_attr(data_array_node, "Dim0", dim0[da_index])
if (dataarray_contains_matrices) { # We only need this attribute if any dataarray has more than 1 dimension. But if so, we need it for all datasets.
xml2::xml_set_attr(data_array_node, "Dim1", dim1[da_index])
}
xml2::xml_set_attr(data_array_node, "Encoding", encoding[da_index])
xml2::xml_set_attr(data_array_node, "DataType", datatype[da_index])
xml2::xml_set_attr(data_array_node, "Intent", intent[da_index])
xml2::xml_set_attr(data_array_node, "Endian", endian[da_index])
xml2::xml_set_attr(data_array_node, "ExternalFileName", "") # not supported atm
xml2::xml_set_attr(data_array_node, "ExternalFileOffset", "") # not supported atm
xml2::xml_set_attr(data_array_node, "ArrayIndexingOrder", array_indexing_order[da_index])
data_array_node_added <- xml2::xml_add_child(root, data_array_node)
data_array_metadata <- xml2::xml_add_child(data_array_node_added, xml2::read_xml("<MetaData></MetaData>"))
encoded_data <- gifti::data_encoder(da, encoding = encoding[da_index], datatype = datatype[da_index], endian = endian[da_index])
data_node <- xml2::read_xml(sprintf("<Data>%s</Data>", encoded_data))
if (num_transform_matrices > 0L) {
tf <- transform_matrix[[da_index]]
if (is.list(tf)) {
tf_node <- xml_node_gifti_coordtransform(tf$transform_matrix, data_space = tf$data_space, transformed_space = tf$transformed_space)
xml2::xml_add_child(data_array_node_added, tf_node)
} else if (is.matrix(tf)) {
tf_node <- xml_node_gifti_coordtransform(tf)
xml2::xml_add_child(data_array_node_added, tf_node)
} else {
if (!is.na(tf)) {
stop(sprintf("Invalid transformation matrix at index %d: neither a named list, nor NA.\n", da_index))
}
}
}
xml2::xml_add_child(data_array_node_added, data_node)
da_index <- da_index + 1L
}
return(root)
} else {
stop("Writing files in GIFTI format requires the 'gifti' package to be installed.")
}
}
#' @title Warn about common errors in combining data and datatype.
#'
#' @param index positive integer, the dataarray index to report. Makes it easier for the user to find the broken one.
#'
#' @param data vector or matrix, the data to write to the GIFTI file. Checked against the datatype.
#'
#' @param datatype NIFTI datatype string, the datatype to use when writing to the GIFTI file. Checked against the data.
#'
#' @param intent NIFTI intent string, checked independently. In no way do we check whether it makes sense for the data.
#'
#' @note The checks in here are in no way exhaustive.
#'
#' @keywords internal
check_data_and_settings_consistency <- function(index, data, datatype, intent, force = FALSE) {
msg <- NULL
if (is.integer(data) & startsWith(datatype, "NIFTI_TYPE_FLOAT")) {
msg <- sprintf("Dataset # %d in file will be corrupted: integer R data passed, and written as '%s'.\n", index, datatype)
}
if (gifti::convert_intent(intent) == "unknown") {
msg <- sprintf("Dataset # %d: Invalid NIFTI intent '%s'.\n", index, intent)
}
# Should check more here.
if (!is.null(msg)) {
if (force) {
warning(msg)
} else {
stop(msg)
}
}
}
#' @title Write XML tree to a gifti file.
#'
#' @param filepath path to the output gifti file
#'
#' @param xmltree XML tree from xml2
#'
#' @param options output options passed to \code{\link[xml2]{write_xml}}.
#'
#' @references \url{https://www.nitrc.org/frs/download.php/2871/GIFTI_Surface_Format.pdf}
#'
#' @examples
#' \dontrun{
#' outfile <- tempfile(fileext = ".gii")
#' my_data_sets <- list(rep(3.1, 3L), matrix(seq(6) + 0.1, nrow = 2L))
#' xmltree <- gifti_xml(my_data_sets, datatype = "NIFTI_TYPE_FLOAT32")
#' # Here we add global metadata:
#' xmltree <- gifti_xml_add_global_metadata(xmltree, list("User" = "Me", "Day" = "Monday"))
#' # Validating your XML never hurts
#' gifti_xsd <- "https://www.nitrc.org/frs/download.php/158/gifti.xsd"
#' xml2::xml_validate(xmltree, xml2::read_xml(gifti_xsd))
#' gifti_xml_write(outfile, xmltree)
#' # Write your custom tree to a file.
#' }
#'
#' @export
#' @importFrom xml2 write_xml
gifti_xml_write <- function(filepath, xmltree, options = c("as_xml", "format")) {
return(invisible(xml2::write_xml(xmltree, file = filepath, options = options)))
}
#' @title Write data to a gifti file.
#'
#' @param filepath path to the output gifti file
#'
#' @param ... parameters passed to \code{\link{gifti_xml}}.
#'
#' @references \url{https://www.nitrc.org/frs/download.php/2871/GIFTI_Surface_Format.pdf}
#'
#' @examples
#' \dontrun{
#' outfile <- tempfile(fileext = ".gii")
#' dataarrays <- list(rep(3.1, 3L), matrix(seq(6), nrow = 2L))
#' gifti_writer(outfile, dataarrays, datatype = c("NIFTI_TYPE_FLOAT32", "NIFTI_TYPE_INT32"))
#' }
#'
#' @export
gifti_writer <- function(filepath, ...) {
if (!is.character(filepath)) {
stop("Parameter 'filepath' must be a character string.")
}
xmltree <- gifti_xml(...)
return(invisible(gifti_xml_write(filepath, xmltree)))
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.