mirror of
https://github.com/ankitects/anki.git
synced 2025-11-26 22:47:12 -05:00
* Refactor: Add index to shapes package * Add shape draw callback API to setupImageCloze * Expose IO drawing API, switch away from image cloze naming We currently use "image occlusion" in most places, but some references to "image cloze" still remain. For consistency's sake and to make it easier to quickly find IO-related code, this commit replaces all remaining references to "image cloze", only maintaining those required for backwards compatibility with existing note types. * Add cloze ordinal to shapes * Do not mutate original shapes during (de)normalization Mutating shapes would be a recipe for trouble when combined with IO API use by external consumers. (makeNormal(makeAbsolute(makeNormal())) is not idempotent, and keeping track of the original state would introduce additional complexity with no discernible performance benefit or otherwise.) * Tweak IO API, allowing modifications to ShapeProperties * Tweak drawShape parameters * Switch method order For consistency with previous implementation * Run Rust formatters * Simplify position (de)normalization --------- Co-authored-by: Glutanimate <glutanimate@users.noreply.github.com>
201 lines
7.1 KiB
Rust
201 lines
7.1 KiB
Rust
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use anki_io::metadata;
|
|
use anki_io::read_file;
|
|
use anki_proto::image_occlusion::get_image_occlusion_note_response::ImageOcclusionNote;
|
|
use anki_proto::image_occlusion::get_image_occlusion_note_response::Value;
|
|
use anki_proto::image_occlusion::AddImageOcclusionNoteRequest;
|
|
use anki_proto::image_occlusion::GetImageForOcclusionResponse;
|
|
use anki_proto::image_occlusion::GetImageOcclusionNoteResponse;
|
|
use anki_proto::image_occlusion::ImageOcclusionFieldIndexes;
|
|
use anki_proto::notetypes::ImageOcclusionField;
|
|
use regex::Regex;
|
|
|
|
use crate::cloze::parse_image_occlusions;
|
|
use crate::media::MediaManager;
|
|
use crate::prelude::*;
|
|
|
|
impl Collection {
|
|
pub fn get_image_for_occlusion(&mut self, path: &str) -> Result<GetImageForOcclusionResponse> {
|
|
let mut metadata = GetImageForOcclusionResponse {
|
|
..Default::default()
|
|
};
|
|
metadata.data = read_file(path)?;
|
|
Ok(metadata)
|
|
}
|
|
|
|
pub fn add_image_occlusion_note(
|
|
&mut self,
|
|
req: AddImageOcclusionNoteRequest,
|
|
) -> Result<OpOutput<()>> {
|
|
// image file
|
|
let image_bytes = read_file(&req.image_path)?;
|
|
let image_filename = Path::new(&req.image_path)
|
|
.file_name()
|
|
.or_not_found("expected filename")?
|
|
.to_str()
|
|
.unwrap()
|
|
.to_string();
|
|
|
|
let mgr = MediaManager::new(&self.media_folder, &self.media_db)?;
|
|
let actual_image_name_after_adding = mgr.add_file(&image_filename, &image_bytes)?;
|
|
|
|
let image_tag = format!(r#"<img src="{}">"#, &actual_image_name_after_adding);
|
|
|
|
let current_deck = self.get_current_deck()?;
|
|
let notetype_id: NotetypeId = req.notetype_id.into();
|
|
self.transact(Op::ImageOcclusion, |col| {
|
|
let nt = if notetype_id.0 == 0 {
|
|
// when testing via .html page, use first available notetype
|
|
col.add_image_occlusion_notetype_inner()?;
|
|
col.get_first_io_notetype()?
|
|
.or_invalid("expected an i/o notetype to exist")?
|
|
} else {
|
|
col.get_io_notetype_by_id(notetype_id)?
|
|
};
|
|
|
|
let mut note = nt.new_note();
|
|
let idxs = nt.get_io_field_indexes()?;
|
|
note.set_field(idxs.occlusions as usize, req.occlusions)?;
|
|
note.set_field(idxs.image as usize, image_tag)?;
|
|
note.set_field(idxs.header as usize, req.header)?;
|
|
note.set_field(idxs.back_extra as usize, req.back_extra)?;
|
|
note.tags = req.tags;
|
|
col.add_note_inner(&mut note, current_deck.id)?;
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub fn get_image_occlusion_note(
|
|
&mut self,
|
|
note_id: NoteId,
|
|
) -> Result<GetImageOcclusionNoteResponse> {
|
|
let value = match self.get_image_occlusion_note_inner(note_id) {
|
|
Ok(note) => Value::Note(note),
|
|
Err(err) => Value::Error(format!("{:?}", err)),
|
|
};
|
|
Ok(GetImageOcclusionNoteResponse { value: Some(value) })
|
|
}
|
|
|
|
pub fn get_image_occlusion_note_inner(
|
|
&mut self,
|
|
note_id: NoteId,
|
|
) -> Result<ImageOcclusionNote> {
|
|
let note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
|
|
let mut cloze_note = ImageOcclusionNote::default();
|
|
|
|
let fields = note.fields();
|
|
|
|
let nt = self
|
|
.get_notetype(note.notetype_id)?
|
|
.or_not_found(note.notetype_id)?;
|
|
let idxs = nt.get_io_field_indexes()?;
|
|
|
|
cloze_note.occlusions = parse_image_occlusions(fields[idxs.occlusions as usize].as_str());
|
|
cloze_note.header = fields[idxs.header as usize].clone();
|
|
cloze_note.back_extra = fields[idxs.back_extra as usize].clone();
|
|
cloze_note.image_data = "".into();
|
|
cloze_note.tags = note.tags.clone();
|
|
|
|
let image_file_name = &fields[idxs.image as usize];
|
|
let src = self
|
|
.extract_img_src(image_file_name)
|
|
.unwrap_or_else(|| "".to_owned());
|
|
let final_path = self.media_folder.join(src);
|
|
|
|
if self.is_image_file(&final_path)? {
|
|
cloze_note.image_data = read_file(&final_path)?;
|
|
}
|
|
|
|
Ok(cloze_note)
|
|
}
|
|
|
|
pub fn update_image_occlusion_note(
|
|
&mut self,
|
|
note_id: NoteId,
|
|
occlusions: &str,
|
|
header: &str,
|
|
back_extra: &str,
|
|
tags: Vec<String>,
|
|
) -> Result<OpOutput<()>> {
|
|
let mut note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
|
|
self.transact(Op::ImageOcclusion, |col| {
|
|
let nt = col
|
|
.get_notetype(note.notetype_id)?
|
|
.or_not_found(note.notetype_id)?;
|
|
let idxs = nt.get_io_field_indexes()?;
|
|
note.set_field(idxs.occlusions as usize, occlusions)?;
|
|
note.set_field(idxs.header as usize, header)?;
|
|
note.set_field(idxs.back_extra as usize, back_extra)?;
|
|
note.tags = tags;
|
|
col.update_note_inner(&mut note)?;
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
fn extract_img_src(&mut self, html: &str) -> Option<String> {
|
|
let re = Regex::new(r#"<img\s+[^>]*src\s*=\s*"([^"]+)"#).unwrap();
|
|
re.captures(html).map(|cap| cap[1].to_owned())
|
|
}
|
|
|
|
fn is_image_file(&mut self, path: &PathBuf) -> Result<bool> {
|
|
let file_path = Path::new(&path);
|
|
let supported_extensions = [
|
|
"jpg", "jpeg", "png", "tif", "tiff", "gif", "svg", "webp", "ico", "avif",
|
|
];
|
|
|
|
if file_path.exists() {
|
|
let meta = metadata(file_path)?;
|
|
if meta.is_file() {
|
|
if let Some(ext_osstr) = file_path.extension() {
|
|
if let Some(ext_str) = ext_osstr.to_str() {
|
|
if supported_extensions.contains(&ext_str.to_lowercase().as_str()) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
impl Notetype {
|
|
pub(crate) fn get_io_field_indexes(&self) -> Result<ImageOcclusionFieldIndexes> {
|
|
get_field_indexes_by_tag(self).or_else(|_| {
|
|
if self.fields.len() < 4 {
|
|
return Err(AnkiError::DatabaseCheckRequired);
|
|
}
|
|
Ok(ImageOcclusionFieldIndexes {
|
|
occlusions: 0,
|
|
image: 1,
|
|
header: 2,
|
|
back_extra: 3,
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
fn get_field_indexes_by_tag(nt: &Notetype) -> Result<ImageOcclusionFieldIndexes> {
|
|
Ok(ImageOcclusionFieldIndexes {
|
|
occlusions: get_field_index(nt, ImageOcclusionField::Occlusions)?,
|
|
image: get_field_index(nt, ImageOcclusionField::Image)?,
|
|
header: get_field_index(nt, ImageOcclusionField::Header)?,
|
|
back_extra: get_field_index(nt, ImageOcclusionField::BackExtra)?,
|
|
})
|
|
}
|
|
|
|
fn get_field_index(nt: &Notetype, field: ImageOcclusionField) -> Result<u32> {
|
|
nt.fields
|
|
.iter()
|
|
.enumerate()
|
|
.find(|(_idx, f)| f.config.tag == Some(field as u32))
|
|
.map(|(idx, _)| idx as u32)
|
|
.ok_or(AnkiError::DatabaseCheckRequired)
|
|
}
|