diff --git a/Cargo.toml b/Cargo.toml
index e2da31f..7573afd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,5 +41,5 @@ anyhow = "1.0.87"
 sha2 = "0.10.8"
 base16ct = { version = "0.2.0", features = ["alloc"] }
 
-# [profile.dev.package."*"]
-# opt-level = 3
+[profile.dev.package."*"]
+opt-level = 3
diff --git a/flake.nix b/flake.nix
index 698aa8b..58766ed 100644
--- a/flake.nix
+++ b/flake.nix
@@ -13,7 +13,12 @@
     inputs.flake-utils.lib.eachSystem (with inputs.flake-utils.lib.system; [ x86_64-linux ]) (
       system:
       let
-        pkgs = inputs.nixpkgs.legacyPackages.${system}.extend (import inputs.rust-overlay);
+        pkgs = inputs.nixpkgs.legacyPackages.${system};
+        # pkgs = inputs.nixpkgs.legacyPackages.${system}.extend (import inputs.rust-overlay);
+        # pkgs = import inputs.nixpkgs {
+        #   inherit system;
+        #   overlays = [ (import inputs.rust-overlay) ];
+        # };
         # toolchain = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default);
         # toolchain = pkgs.rust-bin.stable.latest.default;
         toolchain = inputs.fenix.packages.${system}.complete.toolchain;
@@ -34,18 +39,17 @@
             };
           };
         };
-        devShell = pkgs.mkShell {
+        devShell = pkgs.mkShell rec {
           nativeBuildInputs = with pkgs; [
             toolchain
-            # ruff
-            # imagemagick
+            ruff
+            imagemagick
             pkg-config
 
             # clang
             # llvmPackages.clang
           ];
           buildInputs = with pkgs; [
-            toolchain
             freetype
             fontconfig
             leptonica
@@ -54,7 +58,7 @@
             sqlite
           ];
 
-          # LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
+          LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs;
 
           # compilation of -sys packages requires manually setting LIBCLANG_PATH
           # LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib";
diff --git a/src/arcaea/chart.rs b/src/arcaea/chart.rs
index 21bb03b..8b93689 100644
--- a/src/arcaea/chart.rs
+++ b/src/arcaea/chart.rs
@@ -1,3 +1,4 @@
+use std::path::Path;
 // {{{ Imports
 use std::{fmt::Display, num::NonZeroU16, path::PathBuf};
 
@@ -216,7 +217,7 @@ pub struct Chart {
 
 impl Chart {
 	#[inline]
-	pub fn jacket_path(&self, data_dir: &PathBuf) -> PathBuf {
+	pub fn jacket_path(&self, data_dir: &Path) -> PathBuf {
 		data_dir
 			.join("jackets")
 			.join(format!("{}-{}.jpg", self.song_id, self.id))
@@ -241,10 +242,7 @@ impl CachedSong {
 
 	#[inline]
 	pub fn charts(&self) -> impl Iterator<Item = u32> {
-		self.chart_ids
-			.into_iter()
-			.filter_map(|i| i)
-			.map(|i| i.get() as u32)
+		self.chart_ids.into_iter().flatten().map(|i| i.get() as u32)
 	}
 }
 // }}}
diff --git a/src/arcaea/jacket.rs b/src/arcaea/jacket.rs
index 3b7a7ee..7ea511b 100644
--- a/src/arcaea/jacket.rs
+++ b/src/arcaea/jacket.rs
@@ -55,6 +55,7 @@ impl ImageVec {
 			let r = (r as f64 / count).sqrt();
 			let g = (g as f64 / count).sqrt();
 			let b = (b as f64 / count).sqrt();
+			#[allow(clippy::identity_op)]
 			colors[i as usize * 3 + 0] = r as f32;
 			colors[i as usize * 3 + 1] = g as f32;
 			colors[i as usize * 3 + 2] = b as f32;
diff --git a/src/arcaea/play.rs b/src/arcaea/play.rs
index b2bafe4..e0459f6 100644
--- a/src/arcaea/play.rs
+++ b/src/arcaea/play.rs
@@ -500,7 +500,7 @@ pub fn compute_b30_ptt(scoring_system: ScoringSystem, plays: &PlayCollection<'_>
 }
 // }}}
 // {{{ Maintenance functions
-pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), TaggedError> {
+pub async fn generate_missing_scores(ctx: &UserContext) -> Result<(), Error> {
 	let conn = ctx.db.get()?;
 	let mut query = conn.prepare_cached(
 		"
diff --git a/src/arcaea/score.rs b/src/arcaea/score.rs
index 7ba901e..da93b3b 100644
--- a/src/arcaea/score.rs
+++ b/src/arcaea/score.rs
@@ -226,16 +226,10 @@ impl Score {
 
 		// {{{ Compute score from note breakdown subpairs
 		let pf_score = Score::compute_naive(note_count, pures, fars);
-		let fl_score = Score::compute_naive(
-			note_count,
-			note_count.checked_sub(losts + fars).unwrap_or(0),
-			fars,
-		);
-		let lp_score = Score::compute_naive(
-			note_count,
-			pures,
-			note_count.checked_sub(losts + pures).unwrap_or(0),
-		);
+		let fl_score =
+			Score::compute_naive(note_count, note_count.saturating_sub(losts + fars), fars);
+		let lp_score =
+			Score::compute_naive(note_count, pures, note_count.saturating_sub(losts + pures));
 		// }}}
 		// {{{ Look for consensus among recomputed scores
 		// Lemma: if two computed scores agree, then so will the third
diff --git a/src/assets.rs b/src/assets.rs
index 7b4ea3a..8c9d5ee 100644
--- a/src/assets.rs
+++ b/src/assets.rs
@@ -45,7 +45,7 @@ pub fn get_asset_dir() -> PathBuf {
 fn get_font(name: &str) -> RefCell<Face> {
 	let face = FREETYPE_LIB.with(|lib| {
 		lib.new_face(get_asset_dir().join("fonts").join(name), 0)
-			.expect(&format!("Could not load {} font", name))
+			.unwrap_or_else(|_| panic!("Could not load {} font", name))
 	});
 	RefCell::new(face)
 }
diff --git a/src/bin/cli/commands/analyse.rs b/src/bin/cli/commands/analyse.rs
index 75ba52d..2a82531 100644
--- a/src/bin/cli/commands/analyse.rs
+++ b/src/bin/cli/commands/analyse.rs
@@ -2,6 +2,7 @@
 use std::path::PathBuf;
 
 use crate::context::CliContext;
+use shimmeringmoon::commands::discord::MessageContext;
 use shimmeringmoon::commands::score::magic_impl;
 use shimmeringmoon::context::{Error, UserContext};
 // }}}
@@ -13,6 +14,7 @@ pub struct Args {
 
 pub async fn run(args: Args) -> Result<(), Error> {
 	let mut ctx = CliContext::new(UserContext::new().await?);
-	magic_impl(&mut ctx, &args.files).await?;
+	let res = magic_impl(&mut ctx, &args.files).await;
+	ctx.handle_error(res).await?;
 	Ok(())
 }
diff --git a/src/bin/cli/context.rs b/src/bin/cli/context.rs
index fff301a..5b208c1 100644
--- a/src/bin/cli/context.rs
+++ b/src/bin/cli/context.rs
@@ -3,10 +3,10 @@ use std::num::NonZeroU64;
 use std::path::PathBuf;
 use std::str::FromStr;
 
-use poise::serenity_prelude::{CreateAttachment, CreateMessage};
-
 extern crate shimmeringmoon;
+use poise::CreateReply;
 use shimmeringmoon::assets::get_var;
+use shimmeringmoon::commands::discord::mock::ReplyEssence;
 use shimmeringmoon::context::Error;
 use shimmeringmoon::{commands::discord::MessageContext, context::UserContext};
 // }}}
@@ -53,12 +53,8 @@ impl MessageContext for CliContext {
 		Ok(())
 	}
 
-	async fn send_files(
-		&mut self,
-		_attachments: impl IntoIterator<Item = CreateAttachment>,
-		message: CreateMessage,
-	) -> Result<(), Error> {
-		let all = toml::to_string(&message).unwrap();
+	async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
+		let all = toml::to_string(&ReplyEssence::from_reply(message)).unwrap();
 		println!("\n========== Message ==========");
 		println!("{all}");
 		Ok(())
diff --git a/src/bin/cli/main.rs b/src/bin/cli/main.rs
index 6be8dd0..c426437 100644
--- a/src/bin/cli/main.rs
+++ b/src/bin/cli/main.rs
@@ -1,6 +1,6 @@
 use clap::Parser;
 use command::{Cli, Command};
-use shimmeringmoon::context::{Error, UserContext};
+use shimmeringmoon::context::Error;
 
 mod command;
 mod commands;
diff --git a/src/bin/discord-bot/main.rs b/src/bin/discord-bot/main.rs
index 74a8797..c1663c5 100644
--- a/src/bin/discord-bot/main.rs
+++ b/src/bin/discord-bot/main.rs
@@ -1,5 +1,4 @@
 use poise::serenity_prelude::{self as serenity};
-extern crate shimmeringmoon;
 use shimmeringmoon::arcaea::play::generate_missing_scores;
 use shimmeringmoon::context::{Error, UserContext};
 use shimmeringmoon::{commands, timed};
diff --git a/src/bitmap.rs b/src/bitmap.rs
index a7eee99..7f3f25e 100644
--- a/src/bitmap.rs
+++ b/src/bitmap.rs
@@ -174,9 +174,11 @@ impl BitmapCanvas {
 	}
 
 	// {{{ Draw pixel
+	#[allow(clippy::identity_op)]
 	pub fn set_pixel(&mut self, pos: (u32, u32), color: Color) {
 		let index = 3 * (pos.1 * self.width + pos.0) as usize;
 		let alpha = color.3 as u32;
+
 		self.buffer[index + 0] =
 			((alpha * color.0 as u32 + (255 - alpha) * self.buffer[index + 0] as u32) / 255) as u8;
 		self.buffer[index + 1] =
@@ -299,6 +301,7 @@ impl BitmapCanvas {
 	}
 	// }}}
 	// {{{ Draw text
+	#[allow(clippy::type_complexity)]
 	pub fn plan_text_rendering(
 		pos: Position,
 		faces: &mut [&mut Face],
@@ -490,8 +493,8 @@ impl BitmapCanvas {
 
 		for dx in 0..iw {
 			for dy in 0..ih {
-				let x = pos.0 + dx as i32 + b_glyph.left();
-				let y = pos.1 + dy as i32 - b_glyph.top();
+				let x = pos.0 + dx + b_glyph.left();
+				let y = pos.1 + dy - b_glyph.top();
 
 				// TODO: gamma correction
 				if x >= 0 && (x as u32) < self.width && y >= 0 && (y as u32) < height {
@@ -656,7 +659,7 @@ impl LayoutManager {
 
 		(
 			outer_id,
-			(0..amount.0 * amount.1).into_iter().map(move |i| {
+			(0..amount.0 * amount.1).map(move |i| {
 				let (y, x) = i.div_rem_euclid(&amount.0);
 				((x * inner.width) as i32, (y * inner.height) as i32)
 			}),
@@ -689,7 +692,7 @@ impl LayoutManager {
 	#[inline]
 	pub fn position_relative_to(&self, id: LayoutBoxId, pos: Position) -> Position {
 		let current = self.lookup(id);
-		((pos.0 as i32 + current.x), (pos.1 as i32 + current.y))
+		((pos.0 + current.x), (pos.1 + current.y))
 	}
 
 	#[inline]
diff --git a/src/commands/chart.rs b/src/commands/chart.rs
index 7801247..2e1fc03 100644
--- a/src/commands/chart.rs
+++ b/src/commands/chart.rs
@@ -219,7 +219,7 @@ mod best_tests {
 
 	#[tokio::test]
 	async fn no_scores() -> Result<(), Error> {
-		with_test_ctx!("test/commands/chart/best/specify_difficulty", async |ctx| {
+		with_test_ctx!("test/commands/chart/best/no_scores", async |ctx| {
 			best_impl(ctx, "Pentiment").await?;
 			Ok(())
 		})
@@ -228,9 +228,9 @@ mod best_tests {
 	#[tokio::test]
 	async fn pick_correct_score() -> Result<(), Error> {
 		with_test_ctx!(
-			"test/commands/chart/best/last_byd",
+			"test/commands/chart/best/pick_correct_score",
 			async |ctx: &mut MockContext| {
-				magic_impl(
+				let plays = magic_impl(
 					ctx,
 					&[
 						PathBuf::from_str("test/screenshots/fracture_ray_ex.jpg")?,
@@ -243,6 +243,7 @@ mod best_tests {
 
 				let play = best_impl(ctx, "Fracture ray").await?;
 				assert_eq!(play.score(ScoringSystem::Standard).0, 9_805_651);
+				assert_eq!(plays[0], play);
 
 				Ok(())
 			}
diff --git a/src/commands/discord.rs b/src/commands/discord.rs
index cfb69cb..05336d3 100644
--- a/src/commands/discord.rs
+++ b/src/commands/discord.rs
@@ -122,24 +122,17 @@ impl<'a> MessageContext for poise::Context<'a, UserContext, Error> {
 pub mod mock {
 	use std::{env, fs, path::PathBuf};
 
+	use anyhow::Context;
 	use poise::serenity_prelude::CreateEmbed;
 	use serde::{Deserialize, Serialize};
 	use sha2::{Digest, Sha256};
 
 	use super::*;
 
-	/// A mock context usable for testing. Messages and attachments are
-	/// accumulated inside a vec, and can be used for golden testing
-	/// (see [MockContext::golden])
-	pub struct MockContext {
-		pub user_id: u64,
-		pub data: UserContext,
-		messages: Vec<ReplyEssence>,
-	}
-
+	// {{{ Message essences
 	/// Holds test-relevant data about an attachment.
 	#[derive(Debug, Clone, Serialize, Deserialize)]
-	struct AttachmentEssence {
+	pub struct AttachmentEssence {
 		filename: String,
 		description: Option<String>,
 		/// SHA-256 hash of the file
@@ -148,7 +141,7 @@ pub mod mock {
 
 	/// Holds test-relevant data about a reply.
 	#[derive(Debug, Clone, Serialize)]
-	struct ReplyEssence {
+	pub struct ReplyEssence {
 		reply: bool,
 		ephermal: Option<bool>,
 		content: Option<String>,
@@ -156,6 +149,43 @@ pub mod mock {
 		attachments: Vec<AttachmentEssence>,
 	}
 
+	impl ReplyEssence {
+		pub fn from_reply(message: CreateReply) -> Self {
+			ReplyEssence {
+				reply: message.reply,
+				ephermal: message.ephemeral,
+				content: message.content,
+				embeds: message.embeds,
+				attachments: message
+					.attachments
+					.into_iter()
+					.map(|attachment| AttachmentEssence {
+						filename: attachment.filename,
+						description: attachment.description,
+						hash: {
+							let hash = Sha256::digest(&attachment.data);
+							let string = base16ct::lower::encode_string(&hash);
+
+							// We allocate twice, but it's only at the end of tests,
+							// so it should be fineeeeeeee
+							format!("sha256_{string}")
+						},
+					})
+					.collect(),
+			}
+		}
+	}
+	// }}}
+	// {{{ Mock context
+	/// A mock context usable for testing. Messages and attachments are
+	/// accumulated inside a vec, and can be used for golden testing
+	/// (see [MockContext::golden])
+	pub struct MockContext {
+		pub user_id: u64,
+		pub data: UserContext,
+		messages: Vec<ReplyEssence>,
+	}
+
 	impl MockContext {
 		pub fn new(data: UserContext) -> Self {
 			Self {
@@ -223,28 +253,7 @@ pub mod mock {
 		}
 
 		async fn send(&mut self, message: CreateReply) -> Result<(), Error> {
-			self.messages.push(ReplyEssence {
-				reply: message.reply,
-				ephermal: message.ephemeral,
-				content: message.content,
-				embeds: message.embeds,
-				attachments: message
-					.attachments
-					.into_iter()
-					.map(|attachment| AttachmentEssence {
-						filename: attachment.filename,
-						description: attachment.description,
-						hash: {
-							let hash = Sha256::digest(&attachment.data);
-							let string = base16ct::lower::encode_string(&hash);
-
-							// We allocate twice, but it's only at the end of tests,
-							// so it should be fineeeeeeee
-							format!("sha256_{string}")
-						},
-					})
-					.collect(),
-			});
+			self.messages.push(ReplyEssence::from_reply(message));
 			Ok(())
 		}
 
@@ -266,11 +275,14 @@ pub mod mock {
 		}
 
 		async fn download(&self, attachment: &Self::Attachment) -> Result<Vec<u8>, Error> {
-			let res = tokio::fs::read(attachment).await?;
+			let res = tokio::fs::read(attachment)
+				.await
+				.with_context(|| format!("Could not download attachment {attachment:?}"))?;
 			Ok(res)
 		}
 		// }}}
 	}
+	// }}}
 }
 // }}}
 // {{{ Helpers
diff --git a/src/commands/utils/mod.rs b/src/commands/utils/mod.rs
index b4dacad..916390e 100644
--- a/src/commands/utils/mod.rs
+++ b/src/commands/utils/mod.rs
@@ -1,5 +1,3 @@
-pub mod two_columns;
-
 #[macro_export]
 macro_rules! edit_reply {
     ($ctx:expr, $handle:expr, $($arg:tt)*) => {{
diff --git a/src/commands/utils/two_columns.rs b/src/commands/utils/two_columns.rs
deleted file mode 100644
index 07184d9..0000000
--- a/src/commands/utils/two_columns.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-//! These functions have been copy-pasted from internal `poise` code.
-
-use std::fmt::Write as _;
-
-/// Convenience function to align descriptions behind commands
-pub struct TwoColumnList(Vec<(String, Option<String>)>);
-
-impl TwoColumnList {
-	/// Creates a new [`TwoColumnList`]
-	pub fn new() -> Self {
-		Self(Vec::new())
-	}
-
-	/// Add a line that needs the padding between the columns
-	pub fn push_two_colums(&mut self, command: String, description: String) {
-		self.0.push((command, Some(description)));
-	}
-
-	/// Add a line that doesn't influence the first columns's width
-	pub fn push_heading(&mut self, category: &str) {
-		if !self.0.is_empty() {
-			self.0.push(("".to_string(), None));
-		}
-		let mut category = category.to_string();
-		category += ":";
-		self.0.push((category, None));
-	}
-
-	/// Convert the list into a string with aligned descriptions
-	pub fn into_string(self) -> String {
-		let longest_command = self
-			.0
-			.iter()
-			.filter_map(|(command, description)| {
-				if description.is_some() {
-					Some(command.len())
-				} else {
-					None
-				}
-			})
-			.max()
-			.unwrap_or(0);
-		let mut text = String::new();
-		for (command, description) in self.0 {
-			if let Some(description) = description {
-				let padding = " ".repeat(longest_command - command.len() + 3);
-				writeln!(text, "{}{}{}", command, padding, description).unwrap();
-			} else {
-				writeln!(text, "{}", command).unwrap();
-			}
-		}
-		text
-	}
-}
diff --git a/src/context.rs b/src/context.rs
index 77656eb..e967332 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -9,7 +9,6 @@ use std::sync::LazyLock;
 
 use crate::arcaea::{chart::SongCache, jacket::JacketCache};
 use crate::assets::{get_data_dir, EXO_FONT, GEOSANS_FONT, KAZESAWA_BOLD_FONT, KAZESAWA_FONT};
-use crate::commands::discord::MessageContext;
 use crate::recognition::{hyperglass::CharMeasurements, ui::UIMeasurements};
 use crate::timed;
 // }}}
@@ -48,24 +47,6 @@ macro_rules! get_user_error {
 	}};
 }
 
-/// Handles a [TaggedError], showing user errors to the user,
-/// and throwing away anything else.
-pub async fn discord_error_handler<V>(
-	ctx: &mut impl MessageContext,
-	res: Result<V, TaggedError>,
-) -> Result<Option<V>, Error> {
-	match res {
-		Ok(v) => Ok(Some(v)),
-		Err(e) => match e.kind {
-			ErrorKind::Internal => Err(e.error),
-			ErrorKind::User => {
-				ctx.reply(&format!("{}", e.error)).await?;
-				Ok(None)
-			}
-		},
-	}
-}
-
 impl<E: Into<Error>> From<E> for TaggedError {
 	fn from(value: E) -> Self {
 		Self::new(ErrorKind::Internal, value.into())
diff --git a/src/lib.rs b/src/lib.rs
index 257ab52..f3abf87 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,6 @@
 #![allow(async_fn_in_trait)]
+#![allow(clippy::needless_range_loop)]
+#![allow(clippy::redundant_closure)]
 #![feature(iter_map_windows)]
 #![feature(let_chains)]
 #![feature(array_try_map)]
@@ -7,6 +9,7 @@
 #![feature(thread_local)]
 #![feature(generic_arg_infer)]
 #![feature(iter_collect_into)]
+#![feature(stmt_expr_attributes)]
 
 pub mod arcaea;
 pub mod assets;
diff --git a/src/recognition/hyperglass.rs b/src/recognition/hyperglass.rs
index f254ebf..594aaa0 100644
--- a/src/recognition/hyperglass.rs
+++ b/src/recognition/hyperglass.rs
@@ -1,7 +1,8 @@
-//! Hyperglass is my own specialized OCR system, created as a result of my
-//! annoyance with how unreliable tesseract is. Assuming we know the font,
-//! OCR should be almost perfect, even when faced with stange kerning. This is
-//! what this module achieves!
+//! Hyperglass is my own specialized OCR system.
+//!
+//! Hyperglass was created as a result of my annoyance with how unreliable
+//! tesseract is. Assuming we know the font, OCR should be almost perfect,
+//! even when faced with stange kerning. This is what this module achieves!
 //!
 //! The algorithm is pretty simple:
 //! 1. Find the connected components (i.e., "black areas") in the image.
@@ -202,10 +203,7 @@ impl ComponentsWithBounds {
 		for bound in &mut bounds {
 			if bound.map_or(false, |b| {
 				(b.x_max - b.x_min) as f32 >= max_sizes.0 * image.width() as f32
-			}) {
-				*bound = None;
-			} else if bound.map_or(false, |b| {
-				(b.y_max - b.y_min) as f32 >= max_sizes.1 * image.height() as f32
+					|| (b.y_max - b.y_min) as f32 >= max_sizes.1 * image.height() as f32
 			}) {
 				*bound = None;
 			}
@@ -249,14 +247,14 @@ impl CharMeasurements {
 			weight,
 		};
 		let padding = (5, 5);
-		let planned = BitmapCanvas::plan_text_rendering(padding, &mut [face], style, &string)?;
+		let planned = BitmapCanvas::plan_text_rendering(padding, &mut [face], style, string)?;
 
 		let mut canvas = BitmapCanvas::new(
 			(planned.0 .0) as u32 + planned.1.width + 2 * padding.0 as u32,
 			(planned.0 .1) as u32 + planned.1.height + 2 * padding.0 as u32,
 		);
 
-		canvas.text(padding, &mut [face], style, &string)?;
+		canvas.text(padding, &mut [face], style, string)?;
 		let buffer = ImageBuffer::from_raw(canvas.width, canvas.height(), canvas.buffer.to_vec())
 			.ok_or_else(|| anyhow!("Failed to turn buffer into canvas"))?;
 		let image = DynamicImage::ImageRgb8(buffer);
diff --git a/src/recognition/ui.rs b/src/recognition/ui.rs
index c687f52..d4480d9 100644
--- a/src/recognition/ui.rs
+++ b/src/recognition/ui.rs
@@ -111,14 +111,14 @@ impl UIMeasurements {
 			let i = i % (UI_RECT_COUNT + 2);
 			if i == 0 {
 				for (j, str) in line.split_whitespace().enumerate().take(2) {
-					measurement.dimensions[j] = u32::from_str_radix(str, 10)?;
+					measurement.dimensions[j] = str.parse()?;
 				}
 			} else if i == UI_RECT_COUNT + 1 {
 				measurements.push(measurement);
 				measurement = UIMeasurement::default();
 			} else {
 				for (j, str) in line.split_whitespace().enumerate().take(4) {
-					measurement.datapoints[(i - 1) * 4 + j] = u32::from_str_radix(str, 10)?;
+					measurement.datapoints[(i - 1) * 4 + j] = str.parse()?;
 				}
 			}
 		}
diff --git a/src/transform.rs b/src/transform.rs
index 9272787..4d2a987 100644
--- a/src/transform.rs
+++ b/src/transform.rs
@@ -1,9 +1,11 @@
-//! This file implements the "rotation as shearing" algorithm,
-//! which can rotate images without making use of any trigonometric
-//! functions (or working with floats altogether, if you don't care
+//! This file implements the "rotation as shearing" algorithm.
+//!
+//! The algorithm can rotate images without making use of any trigonometric
+//! functions (or working with floats altogether, assuming you don't care
 //! about antialiasing).
 //!
-//! For more information, consult this article: https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html
+//! For more information, consult this article:
+//! https://www.ocf.berkeley.edu/~fricke/projects/israel/paeth/rotation_by_shearing.html
 
 use image::{DynamicImage, GenericImage, GenericImageView};
 
@@ -57,7 +59,7 @@ pub fn yshear(image: &mut DynamicImage, rect: Rect, center: Position, shear: f32
 	}
 }
 
-/// Performs a rotation as a series of three shear operations
+/// Performs a rotation as a series of three shear operations.
 /// Does not perform anti-aliasing.
 pub fn rotate(image: &mut DynamicImage, rect: Rect, center: Position, angle: f32) {
 	let alpha = -f32::tan(angle / 2.0);