Anki/rslib/src/image_occlusion/imagedata.rs
Abdo 495dc4d87c Add text tool to image occlusion (#2705)
* Add text tool to IO

* Remove unnecessary parentheses

* Fix text objects always grouped

* Remove log

* Fix text objects hidden on back side

* Implement text scaling

* Add inverse text outline

* Warn about IO notes with only text objects

This will result in a different error message than the case where no
objects are added at all though, and the user can bypass the warning.
Maybe this is better to avoid discarding the user's work if they have
spent some time adding text.

* Add isValidType

* Use matches!

* Lock aspect ratio of text objects

* Reword misleading comment

The confusion probably comes from the Fabric docs, which apparently need updating: http://fabricjs.com/docs/fabric.Canvas.html#uniformScaling

* Do not count text objects when calculating current index

* Make text objects respond to size changes

* Fix uniform scaling not working when editing

* Use Arial font

* Escape colons and unify parsing

* Handle scale factor when restricting shape to view

* Use 'cloned'

* Add text background

* Tweak drawShape's params
2023-10-12 13:40:11 +10:00

198 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::ImageClozeNote;
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<ImageClozeNote> {
let note = self.storage.get_note(note_id)?.or_not_found(note_id)?;
let mut cloze_note = ImageClozeNote::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)
}