Skip to content

Commit

Permalink
add form generation via typst
Browse files Browse the repository at this point in the history
this closes #3
  • Loading branch information
o-tho committed Nov 20, 2024
1 parent 2167387 commit 3dbb54e
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 25 deletions.
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ which is self-documented. The following is for the wasm web application.

We need a template which tells us where every answer box is located on the page
and where three large round circles on the page are to identify the position of
a scan. For this, go to the *Create Template* view. Upload an image file (like .png) of
a scan.

You have two options for this. You can either use the *Create Form* view, which
generates a PDF file to print (in A4) and the corresponding `template.json` file
that you need to provide when you grade the exam.

#### Using a custom form

If you want to use a custom form, this is entirely possible, but significantly
more painful.

For this, go to the *Create Template* view. Upload an image file (like .png) of
an empty form, preferably directly converted from a PDF file so nothing is
skewed by a scanner. You need to enter the following information:

Expand All @@ -61,6 +72,10 @@ After entering this data and uploading an image file, you can hit "Preview" to
see if everything goes well. You can then download the configuration to
`template.json`

If this does not work in your case, you will need to adapt the `template.json`
by hand and provide the coordinates of the bounding boxes of all bubbles (top
left corner and bottom right corner).

### The exam key

Go to *Create Key*. Enter how many versions you have and enter the correct
Expand Down Expand Up @@ -103,5 +118,5 @@ correct answer, meaning that the selected bubble is elsewhere.

## Acknowledgements

This project uses ![typst](https://github.com/typst/typst) for typesetting and
This project uses [typst](https://github.com/typst/typst) for typesetting and
ships with copies of the Linux Biolinum font by Philipp H Poll.
Binary file modified assets/filled_out_example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified assets/sample_report.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions src/webapp/create_form.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
use crate::image_helpers::binary_image_from_image;
use crate::scan::Scan;
use crate::template::Template;
use crate::typst_helpers::{typst_frame_to_template, TypstWrapper};
use crate::webapp::utils::{download_button, QuestionSettings};
use eframe::egui::{Context, ScrollArea};
use eframe::Frame;

pub struct CreateForm {
pub question_settings: QuestionSettings,
pub preview: Option<egui::TextureHandle>,
pub pdf: Option<Vec<u8>>,
pub template: Option<Template>,
pub png: Option<Vec<u8>>,
}

impl Default for CreateForm {
fn default() -> Self {
Self {
question_settings: QuestionSettings::default(),
preview: None,
pdf: None,
template: None,
png: None,
}
}
}

impl CreateForm {
pub fn update(&mut self, ctx: &Context, _frame: &mut Frame) {
eframe::egui::SidePanel::left("settings_panel")
.resizable(false)
.default_width(200.0)
.show(ctx, |ui| {
ui.heading("Settings");
ui.add_space(10.0);

ui.add(
egui::Slider::new(&mut self.question_settings.num_qs, 1..=50)
.text("Number of Questions"),
);

ui.add(
egui::Slider::new(&mut self.question_settings.num_id_qs, 1..=15)
.text("ID Questions"),
);

ui.add(
egui::Slider::new(&mut self.question_settings.num_versions, 1..=8)
.text("Versions"),
);

ui.add(
egui::Slider::new(&mut self.question_settings.num_answers, 2..=8)
.text("Answers per Question"),
);

ui.add_space(20.0);

if ui.button("Generate").clicked() {
let tmpl = include_str!("../../assets/formtemplate.typ");

let code = format!(
r#"
#let num_qs = {}
#let num_idqs = {}
#let num_answers = {}
#let num_versions = {}
{}
"#,
self.question_settings.num_qs,
self.question_settings.num_id_qs,
self.question_settings.num_answers,
self.question_settings.num_versions,
tmpl
);
let wrapper = TypstWrapper::new(code);

let document = typst::compile(&wrapper)
.output
.expect("Error from Typst. This really should not happen. So sorry.");

let scale = 3.0;
let template = typst_frame_to_template(&document.pages[0].frame, scale);
self.template = Some(template.clone());
let pdf =
typst_pdf::pdf(&document, &typst_pdf::PdfOptions::default()).expect("bla");

self.pdf = Some(pdf.clone());

let png = typst_render::render(&document.pages[0], scale as f32)
.encode_png()
.unwrap();

self.png = Some(png.clone());

let dynimage = image::load_from_memory(&png).unwrap();
let scan = Scan {
img: binary_image_from_image(dynimage),
transformation: None,
};
let circled = scan.circle_everything(&template);
let dynamic_image = image::DynamicImage::ImageRgb8(circled);

let size = [dynamic_image.width() as _, dynamic_image.height() as _];
let image_buffer = dynamic_image.to_rgba8();
let pixels = image_buffer.as_flat_samples();

let texture = ctx.load_texture(
"preview_image",
egui::ColorImage::from_rgba_unmultiplied(size, pixels.as_slice()),
egui::TextureOptions::default(),
);
self.preview = Some(texture);

download_button(
ui,
"💾 Save Template as json",
serde_json::to_vec(&template).unwrap(),
);
download_button(ui, "💾 Save form as PNG", png);
}

if let Some(template) = &self.template {
download_button(
ui,
"💾 Save template as JSON",
serde_json::to_vec(&template).unwrap(),
);
}
if let Some(pdf) = &self.pdf {
download_button(ui, "💾 Save form as PDF", pdf.to_vec());
}
if let Some(png) = &self.png {
download_button(ui, "💾 Save form as PNG", png.to_vec());
}
});

eframe::egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Preview");
if let Some(texture) = &self.preview {
ScrollArea::both().show(ui, |ui| {
// Display the rendered image or a placeholder if no image is available
ui.add(egui::Image::new(texture));
});
} else {
ui.label("No preview available");
}
});
}
}
23 changes: 3 additions & 20 deletions src/webapp/create_template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use crate::image_helpers::{binary_image_from_image, rgb_to_egui_color_image};
use crate::point::Point;
use crate::scan::Scan;
use crate::template::Template;
use crate::webapp::utils::{download_button, template_from_settings, upload_button, FileType};
use crate::webapp::utils::{
download_button, template_from_settings, upload_button, FileType, QuestionSettings,
};
use eframe::egui::{CentralPanel, Context, ScrollArea, SidePanel, TextEdit, Ui};
use eframe::Frame;
use tokio::sync::mpsc::{channel, Receiver, Sender};
Expand Down Expand Up @@ -265,25 +267,6 @@ impl CreateTemplate {
});
}
}

pub struct QuestionSettings {
pub num_qs: u32,
pub num_id_qs: u32,
pub num_versions: u32,
pub num_answers: u32,
}

impl Default for QuestionSettings {
fn default() -> Self {
Self {
num_qs: 20,
num_id_qs: 9,
num_versions: 4,
num_answers: 5,
}
}
}

pub struct LayoutSettings {
pub box_height: u32,
pub box_width: u32,
Expand Down
1 change: 1 addition & 0 deletions src/webapp/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![cfg(target_arch = "wasm32")]
pub mod create_form;
pub mod create_key;
pub mod create_template;
pub mod generate_report;
Expand Down
22 changes: 19 additions & 3 deletions src/webapp/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ use crate::point::Point;
use crate::template::Box;
use crate::template::Question;
use crate::template::Template;
use crate::webapp::create_template::{
CircleSettings, LayoutSettings, PositionSettings, QuestionSettings,
};
use crate::webapp::create_template::{CircleSettings, LayoutSettings, PositionSettings};
use std::future::Future;
use tokio::sync::mpsc::Sender;

Expand All @@ -18,6 +16,24 @@ pub fn execute<F: Future<Output = ()> + 'static>(f: F) {
wasm_bindgen_futures::spawn_local(f);
}

pub struct QuestionSettings {
pub num_qs: u32,
pub num_id_qs: u32,
pub num_versions: u32,
pub num_answers: u32,
}

impl Default for QuestionSettings {
fn default() -> Self {
Self {
num_qs: 20,
num_id_qs: 9,
num_versions: 4,
num_answers: 5,
}
}
}

pub fn upload_button(
ui: &mut egui::Ui,
ctx: &egui::Context,
Expand Down
8 changes: 8 additions & 0 deletions src/webapp/webapp.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use eframe::egui;
use eframe::egui::Context;

use crate::webapp::create_form::CreateForm;
use crate::webapp::create_key::CreateKey;
use crate::webapp::create_template::CreateTemplate;

Expand All @@ -11,13 +12,15 @@ pub struct WebApp {
current_view: ViewType,
generate_report: GenerateReport,
create_template: CreateTemplate,
create_form: CreateForm,
create_key: CreateKey,
help: Help,
}

enum ViewType {
GenerateReport,
CreateTemplate,
CreateForm,
CreateKey,
Help,
}
Expand All @@ -27,6 +30,7 @@ impl Default for WebApp {
Self {
current_view: ViewType::Help,
generate_report: GenerateReport::default(),
create_form: CreateForm::default(),
create_template: CreateTemplate::default(),
create_key: CreateKey::default(),
help: Help::default(),
Expand All @@ -42,6 +46,9 @@ impl eframe::App for WebApp {
if ui.button("Generate Report").clicked() {
self.current_view = ViewType::GenerateReport;
}
if ui.button("Create Form").clicked() {
self.current_view = ViewType::CreateForm;
}
if ui.button("Create Template").clicked() {
self.current_view = ViewType::CreateTemplate;
}
Expand All @@ -56,6 +63,7 @@ impl eframe::App for WebApp {

match self.current_view {
ViewType::GenerateReport => self.generate_report.update(ctx, frame),
ViewType::CreateForm => self.create_form.update(ctx, frame),
ViewType::CreateTemplate => self.create_template.update(ctx, frame),
ViewType::CreateKey => self.create_key.update(ctx, frame),
ViewType::Help => self.help.update(ctx, frame),
Expand Down

0 comments on commit 3dbb54e

Please sign in to comment.