bdf-reader 0.1.1

BDF font format reader
Documentation
use paste::paste;
use std::str::FromStr;
use thiserror::Error;

#[derive(Debug, Error)]
#[error("No such variant: {0}")]
pub struct NoSuchVariant(String);

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WritingDirection {
	Horizontal = 0,
	Vertical = 1,
	Both = 2
}

impl FromStr for WritingDirection {
	type Err = NoSuchVariant;

	fn from_str(s: &str) -> Result<Self, Self::Err> {
		match s {
			"0" => Ok(Self::Horizontal),
			"1" => Ok(Self::Vertical),
			"2" => Ok(Self::Both),
			_ => Err(NoSuchVariant(s.into()))
		}
	}
}

macro_rules! tokens {
	(
		$(#[doc$($doc:tt)*])*
		$vis:vis enum $ident:ident {
			$(
				$(#[doc$($variant_doc:tt)*])*
				$(#[test($test_input:literal, $test_expected:expr)])*
				$variant:ident {
					$tag:literal $(,
						$($arg:ident: $arg_ty:ident),* $(,)?
						$(..$remaining:ident)?
					)?
				}
			),*
		}
	) => {
		paste! {
			$(#[doc$($doc)*])*
			#[allow(clippy::derive_partial_eq_without_eq)]
			#[derive(Clone, Debug, PartialEq)]
			$vis enum $ident {
				$(
					$(#[doc$($variant_doc)*])*
					$variant $({ $($arg: $arg_ty,)* $($remaining: String)? })?
				),*
			}

			#[derive(Debug, Error)]
			pub enum Error {
				$(
					$(
						$(
							#[error(
								"Failed to parse argument {} of tag {}: {0}",
								stringify!($arg),
								$tag
							)]
							[<$variant $arg:camel>](#[source] <$arg_ty as FromStr>::Err),
						)*
					)?
				)*

				#[error("Missing the argument {1} of tag {0}")]
				MissingArg(&'static str, &'static str),

				#[error("Extra tokens after tag {0}")]
				ExtraTokens(&'static str),

				#[error("Unknown tag: {0}")]
				UnknownTag(String)
			}

			impl $ident {
				#[allow(unused_assignments, unused_mut)]
				$vis fn parse_line(line: &str) -> Result<Self, Error> {
					let mut tokens = line
						.trim_end()
						.split(|ch: char| ch.is_ascii_whitespace())
						.peekable();

					$(
						if tokens.peek() == Some(&$tag) {
							tokens.next().unwrap();
							let mut empty = tokens.peek().is_none();
							$(
								$(
									let $arg: $arg_ty = tokens
										.next()
										.ok_or(Error::MissingArg($tag, stringify!($arg)))?
										.parse()
										.map_err(Error::[<$variant $arg:camel>])?;
									empty = tokens.peek().is_none();
								)*
								$(
									let $remaining = tokens
										.fold(String::new(), |mut rem, token| {
											if !rem.is_empty() {
												rem += " ";
											}
											rem += token;
											rem
										});
									empty = true;
								)?
							)?
							if !empty {
								return Err(Error::ExtraTokens($tag));
							}
							return Ok(Self::$variant $({ $($arg,)* $($remaining)? })?);
						}
					)*

					return Err(Error::UnknownTag(tokens.next().unwrap().into()));
				}
			}

			$(
				#[cfg(test)]
				#[test]
				fn [<test_ $ident:lower _parse_ $variant:lower>]() {
					$(
						let expected: $ident = {
							use $ident::*;
							$test_expected
						};
						assert_eq!(expected, $ident::parse_line($test_input).unwrap());
					)*
				}
			)*
		}
	};
}

// https://adobe-type-tools.github.io/font-tech-notes/pdfs/5005.BDF_Spec.pdf
tokens! {
	pub enum Token {
		/// `STARTFONT` is followed by a version number indicating the exact file format
		/// used (for example, 2.1).
		#[test("STARTFONT 2.1", StartFont { ver: "2.1".into() })]
		StartFont { "STARTFONT", ver: String },

		/// One or more lines beginning with the word `COMMENT`. These lines can be
		/// ignored by any program reading the file.
		#[test("COMMENT hello world", Comment { comment: "hello world".into() })]
		Comment { "COMMENT", ..comment },

		/// (Optional) The value of `CONTENTVERSION` is an integer which can be
		/// assigned by an installer program to keep track of the version of the included
		/// data. The value is intended to be valid only in a single environment, under
		/// the control of a single installer. The value of `CONTENTVERSION` should only
		/// reflect upgrades to the quality of the bitmap images, not to the glyph
		/// complement or encoding.
		#[test("CONTENTVERSION 1", ContentVersion { ver: 1 })]
		ContentVersion { "CONTENTVERSION", ver: i32 },

		/// `FONT` is followed by the font name, which should exactly match the
		/// Post-Script™ language **FontName** in the corresponding outline font program
		#[test("FONT font-name", Font { name: "font-name".into() })]
		Font { "FONT", name: String },

		/// `SIZE` is followed by the point size of the glyphs and the x and y resolutions
		/// of the device for which the font is intended.
		#[test("SIZE 16 75 75", Size { pt: 16, xres: 75, yres: 75 })]
		Size { "SIZE", pt: u32, xres: u32, yres: u32 },

		/// `FONTBOUNDINGBOX` is followed by the width in x and the height in y, and
		/// the x and y displacement of the lower left corner from origin 0 (for
		/// horizontal writing direction); all in integer pixel values.
		#[test(
			"FONTBOUNDINGBOX 16 16 0 -2",
			FontBoundingBox { fbbx: 16, fbby: 16, xoff: 0, yoff: -2 }
		)]
		FontBoundingBox { "FONTBOUNDINGBOX", fbbx: u32, fbby: u32, xoff: i32, yoff: i32 },

		/// (Optional) The integer value of `METRICSSET  may be 0, 1, or 2, which
		/// scorrepond to writing direction 0 only, 1 only, or both (respectively). If not
		/// present, `METRICSSET` 0 is implied. If `METRICSSET` is 1, `DWIDTH` and
		/// `SWIDTH` keywords are optional.
		#[test("METRICSSET 0", MetricsSet { dir: WritingDirection::Horizontal })]
		#[test("METRICSSET 1", MetricsSet { dir: WritingDirection::Vertical })]
		#[test("METRICSSET 2", MetricsSet { dir: WritingDirection::Both })]
		MetricsSet { "METRICSSET", dir: WritingDirection },

		/// `SWIDTH` is followed by swx0 and swy0, the scalable width of the glyph in x
		/// and y for writing mode 0. The scalable widths are of type Number and are in
		/// units of 1/1000th of the size of the glyph and correspond to the widths found
		/// in AFM files (for outline fonts). If the size of the glyph is p points, the
		/// width information must be scaled by p/1000 to get the width of the glyph in
		/// printer’s points. This width information should be regarded as a vector
		/// indicating the position of the next glyph’s origin relative to the origin of
		/// this glyph. `SWIDTH` is mandatory for all writing mode 0 fonts.
		///
		/// To convert the scalable width to the width in device pixels, multiply `SWIDTH`
		/// times p/1000 times r/72, where r is the device resolution in pixels per inch.
		/// The result is a real number giving the ideal width in device pixels. The
		/// actual device width must be an integral number of device pixels and is given
		/// by the `DWIDTH` entry.
		#[test("SWIDTH 1000 0", SWidth { swx0: 1000.0, swy0: 0.0 })]
		SWidth { "SWIDTH", swx0: f64, swy0: f64 },

		/// `DWIDTH` specifies the widths in x and y, dwx0 and dwy0, in device pixels.
		/// Like `SWIDTH`, this width information is a vector indicating the position of
		/// the next glyph’s origin relative to the origin of this glyph. `DWIDTH` is
		/// mandatory for all writing mode 0 fonts.
		#[test("DWIDTH 16 0", DWidth { dwx0: 16.0, dwy0: 0.0 })]
		DWidth { "DWIDTH", dwx0: f64, dwy0: f64 },

		/// `SWIDTH1` is followed by the values for swx1 and swy1, the scalable width of
		/// the glyph in x and y, for writing mode 1 (vertical direction). The values are
		/// of type Number, and represent the widths in glyph space coordinates.
		#[test("SWIDTH1 1000 0", SWidthVertical { swx1: 1000.0, swy1: 0.0 })]
		SWidthVertical { "SWIDTH1", swx1: f64, swy1: f64 },

		/// `DWIDTH1` specifies the integer pixel width of the glyph in x and y. Like
		/// `SWIDTH1`, this width information is a vector indicating the position of the
		/// next glyph’s origin relative to the origin of this glyph. `DWIDTH1` is
		/// mandatory for all writing mode 1 fonts.
		#[test("DWIDTH1 16 0", DWidthVertical { dwx1: 16.0, dwy1: 0.0 })]
		DWidthVertical { "DWIDTH1", dwx1: f64, dwy1: f64 },

		/// `VVECTOR` (optional) specifies the components of a vector from origin 0 (the
		/// origin for writing direction 0) to origin 1 (the origin for writing direction
		/// 1). If the value of `METRICSSET` is 1 or 2, `VVECTOR` must be specified either
		/// at the global level, or for each individual glyph. If specified at the global
		/// level, the `VVECTOR` is the same for all glyphs, though the inclusion of this
		/// keyword in an individual glyph has the effect of overriding the bal value for
		/// that specific glyph.
		#[test("VVECTOR 1 1", VVector { xoff: 1.0, yoff: 1.0 })]
		VVector { "VVECTOR", xoff: f64, yoff: f64 },

		/// The optional word `STARTPROPERTIES` may be followed by the number of
		/// properties (n) that follow. Within the properties list, there may be n lines
		/// consisting of a word for the property name followed by either an integer or
		/// string surrounded by ASCII double quotation marks (ASCII octal 042).
		/// Internal quotation characters are indicated (or “quoted”) by using two
		/// quotation characters in a row.
		#[test("STARTPROPERTIES 1", StartProperties { n: 1 })]
		StartProperties { "STARTPROPERTIES", n: usize },

		/// The word `ENDPROPERTIES` is used to delimit the end of the optional properties
		/// list in fonts files containing the word `STARTPROPERTIES`.
		#[test("ENDPROPERTIES", EndProperties {})]
		EndProperties { "ENDPROPERTIES" },

		/// `CHARS` is followed by *nglyphs*, the number of glyphs that follow. To make
		/// sure that the correct number of glyphs were actually read and processed, error
		/// checking is recommended at the end of the file
		#[test("CHARS 1", Chars { nglyphs: 1 })]
		Chars { "CHARS", nglyphs: usize },

		/// The word `STARTCHAR` followed by a string containing the name for the
		/// glyph. In base fonts, this should correspond to the name in the PostScript
		/// language outline font’s encoding vector. In a Composite font (Type 0), the
		/// value may be a numeric offset or glyph ID.
		#[test("STARTCHAR U+0041", StartChar { name: "U+0041".into() })]
		StartChar { "STARTCHAR", name: String },

		/// `ENCODING` is followed by a positive integer representing the Adobe Standard
		/// Encoding value. If the character is not in the Adobe Standard Encoding,
		/// `ENCODING` is followed by –1 and optionally by another integer specifying
		/// the glyph index for the non-standard encoding.
		// TODO represent the optional encoding value
		#[test("ENCODING 65", Encoding { enc: 65 })]
		Encoding { "ENCODING", enc: u32 },

		/// `BBX` is followed by BBw, the width of the black pixels in x, and BBh, the
		/// height in y. These are followed by the x and y displacement, BBxoff0 and
		/// BByoff0, of the lower left corner of the bitmap from origin 0. All values are
		/// are an integer number of pixels.
		///
		/// If the font specifies metrics for writing direction 1, `VVECTOR` specifies the
		/// offset from origin 0 to origin 1. For example, for writing direction 1, the
		/// offset from origin 1 to the lower left corner of the bitmap would be:
		///
		/// BBxoff1x,y = BBxoff0x,y – `VVECTOR`
		#[test("BBX 16 16 0 -2", BoundingBox { bbw: 16, bbh: 16, bbxoff: 0, bbyoff: -2 })]
		BoundingBox { "BBX", bbw: u32, bbh: u32, bbxoff: i32, bbyoff: i32 },

		/// `BITMAP` introduces the hexadecimal data for the character bitmap. From the
		/// `BBX` value for h, find h lines of hex-encoded bitmap, padded on the right
		/// with zero’s to the nearest byte (that is, multiple of 8). Hex data can be
		/// turned into binary by taking two bytes at a time, each of which represents 4
		/// bits of the 8-bit value. For example, the byte 01101101 is two hex digits: 6
		/// (0110 in hex) and D (1101 in hex).
		#[test("BITMAP", Bitmap {})]
		Bitmap { "BITMAP" },

		/// `ENDCHAR` delimits the end of the glyph description
		#[test("ENDCHAR", EndChar {})]
		EndChar { "ENDCHAR" },

		/// The entire file is terminated with the word `ENDFONT`. If this is encountered
		/// before all of the glyphs have been read, it is an error cond
		#[test("ENDFONT", EndFont {})]
		EndFont { "ENDFONT" }
	}
}