///|
fn ascii_text(text : String, record_index : Int) -> Result[Unit, FitsError] {
for ch in text {
let value = ch.to_int()
if value < 0x20 || value > 0x7e {
return Err(
InvalidCard(record_index, "encoded text must be printable ASCII"),
)
}
}
Ok(())
}
///|
fn spaces(count : Int) -> String {
let output = StringBuilder()
for index = 0; index < count; index = index + 1 {
output.write_char(' ')
}
output.to_string()
}
///|
fn keyword_field(keyword : String) -> String {
keyword + spaces(8 - keyword.length())
}
///|
fn canonical_value(value : String) -> String {
if value == "" || value[0] == '\'' || value.length() >= 20 {
value
} else {
spaces(20 - value.length()) + value
}
}
///|
fn encode_card_text(
card : Card,
record_index : Int,
) -> Result[String, FitsError] {
if card.keyword.length() > 8 {
return Err(InvalidKeyword(record_index, card.keyword))
}
match validate_keyword(card.keyword, record_index) {
Err(error) => return Err(error)
Ok(_) => ()
}
if card.keyword == "" && card.value is Some(_) {
return Err(InvalidCard(record_index, "a blank keyword cannot have a value"))
}
let text = match card.value {
Some(value) => {
let mut output = keyword_field(card.keyword) +
"= " +
canonical_value(value)
match card.comment {
None => ()
Some(comment) => output = output + " / " + comment
}
output
}
None => {
let comment = match card.comment {
None => ""
Some(value) => value
}
keyword_field(card.keyword) + comment
}
}
match ascii_text(text, record_index) {
Err(error) => return Err(error)
Ok(_) => ()
}
if text.length() > fits_card_size {
return Err(CardTooLong(record_index, text.length()))
}
Ok(text)
}
///|
fn append_ascii(output : Array[Byte], text : String) -> Unit {
for ch in text {
output.push(ch.to_int().to_byte())
}
}
///|
/// Deterministically encode a parsed FITS header.
///
/// Values retain their lexical representation. Non-string values shorter than
/// 20 bytes are right-aligned in the conventional FITS value field; quoted
/// strings begin immediately after `= `. Every card is padded to 80 bytes and
/// the completed header is padded with spaces to a 2880-byte boundary.
pub fn encode_header(header : Header) -> Result[Bytes, FitsError] {
if header.cards.length() >
(2147483647 - (fits_block_size - 1)) / fits_card_size {
return Err(SizeOverflow("encoded header"))
}
let output = []
let mut found_end = false
for index, card in header.cards {
if found_end {
return Err(InvalidCard(index, "cards cannot follow END"))
}
if card.keyword == "END" {
if card.value is Some(_) || card.comment is Some(_) {
return Err(InvalidCard(index, "END must not have a value or comment"))
}
found_end = true
}
let text = match encode_card_text(card, index) {
Err(error) => return Err(error)
Ok(value) => value
}
append_ascii(output, text)
for position = text.length()
position < fits_card_size
position = position + 1 {
output.push(b' ')
}
}
if !found_end {
return Err(MissingEnd(0))
}
while output.length() % fits_block_size != 0 {
output.push(b' ')
}
Ok(Bytes::from_array(output))
}
///|
fn append_bytes(output : Array[Byte], bytes : Bytes) -> Unit {
for byte in bytes {
output.push(byte)
}
}
///|
fn encode_hdu(
header : Header,
data : Bytes,
hdu_index : Int,
context : String,
) -> Result[Bytes, FitsError] {
let layout = match analyze_hdu_header(header, hdu_index) {
Err(error) => return Err(error)
Ok(value) => value
}
if layout.data_length != data.length() {
return Err(InvalidDataLength(layout.data_length, data.length()))
}
let header_bytes = match encode_header(header) {
Err(error) => return Err(error)
Ok(value) => value
}
let padded_data_length = match checked_padded_block_length(data.length()) {
Err(error) => return Err(error)
Ok(value) => value
}
let output_length = match
checked_sum(header_bytes.length(), padded_data_length, context) {
Err(error) => return Err(error)
Ok(value) => value
}
let output = Array(capacity=output_length)
append_bytes(output, header_bytes)
append_bytes(output, data)
while output.length() < output_length {
output.push(b'\x00')
}
Ok(Bytes::from_array(output))
}
///|
/// Assemble one complete primary HDU from a header and encoded data unit.
///
/// Structural cards and the exact unpadded data length are validated with the
/// same rules used by `parse_fits`. Header padding is ASCII space; data padding
/// is zero. The supplied header's stored `data_offset` is ignored because the
/// canonical encoded header determines the output offset.
pub fn encode_primary_hdu(
header : Header,
data : Bytes,
) -> Result[Bytes, FitsError] {
encode_hdu(header, data, 0, "encoded primary HDU")
}
///|
/// Assemble one complete extension HDU from a header and encoded data unit.
///
/// The header must begin with `XTENSION` and contain ordered `BITPIX`, `NAXIS`,
/// each `NAXISn`, `PCOUNT`, and `GCOUNT` cards. Both IMAGE and non-image
/// extensions use the general FITS data-length formula before zero padding.
pub fn encode_extension_hdu(
header : Header,
data : Bytes,
) -> Result[Bytes, FitsError] {
encode_hdu(header, data, 1, "encoded extension HDU")
}