Typstに機能追加してみたい! (大規模OSSを手探る-進捗ブログ)
2日目
なにをやるか
- ソフトウェア: Typst
- やりたいこと: LaTeXの
\includegraphics
にはangle
オプションがあるが、typstのimage()
関数にはangleに類するオプションが無いため、その実装をめざしたい。え? angleオプション使ったことないって? 知らん
buildまで
とりあえずgithubからTypstをクローンし開発用のレポジトリ(プライベートだが)におく。 その後手元にpullしてbranchを切り、まずはbuildしてみた。
$ git clone git@github.com:path/to.git
$ git branch dev
$ git checkout dev
$ cargo build
Compilling hoge
Compilling fuwa
…
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2m 55s
(意外と時間はかからなかったが)buildできた。これが実行できるか確認する。
$ ./target/debug/typst
The Typst compiler
Usage: typst [OPTIONS] <COMMAND>
…
ビルドには無事成功したらしい。
以降の目標
- debuggerの準備:ある記事曰くVS-Code上でrust-analyzerとCodeLLDBの組みあわせがよさそう?
- rustの勉強: rust本の所有権~構造体までは学びたい
- 先駆者の記事を読む:先駆者様を参照してどのようなことをすればいいか把握する。
- Typstの挙動確認: hoge.typをいれたときにどうなるかを確認。 ここらへん を参照
雑談
なんも考えずにcargo build
を実行して/target
を直下につくってしまったときには、git的に大変なことになってしまうのかと恐れたが、.gitignore
で/target
が無視になっていてたすかった。ありがとう.gitignore
3日目
rustbookを読むだけの一日となった。第7章まで読んだので次回からはデバッギングに移行したい。
Rust:
Good for safety, need a PHD to print Hello World
4日目
デバッグ環境
CodeLLDB + Rustを使用。./.vscode/launch.JSON
以下に既に色々デバッグやユニットテストがあったが、今回は以下を追記した。
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug /target/debug/typst",
"program": "${workspaceFolder}/target/debug/typst",
"args": ["compile", "/path/to/*.typ"],
"cwd": "${workspaceFolder}",
"sourceLanguages": ["rust"],
"preLaunchTask": "rust: cargo build",
}
],
"rust-analyzer.linkedProjects": ["${workspaceFolder}/Cargo.toml"],
}
とりあえずわかったこと
fn main() -> ExitCode {
let res = dispatch();
if let Err(msg) = res {
set_failed();
print_error(msg.message()).expect("failed to print error");
}
EXIT.with(|cell| cell.get())
}
から開始。 dispatch()
内でErrorが返されたらprint_Error
をしてExit
。つまりコンパイルはdispatch()
内でおこってる。
fn dispatch() -> HintedStrResult<()> {
let timer = Timer::new(&ARGS);
match &ARGS.command {
Command::Compile(command) => crate::compile::compile(timer, command.clone())?,
Command::Watch(command) => crate::watch::watch(timer, command.clone())?,
Command::Init(command) => crate::init::init(command)?,
Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command)?,
}
Ok(())
}
わかりやすいMatch文がでてきた。しかもこのMatch文、typst
に変数を渡さずに走らせたときにでるヘルプとまったく同じ。
ほなこのMatchでコマンドみてるんやろ
じゃあ渡されているARGSは何だろうと思って、型のCliArgument
を見にいく
pub struct CliArguments {
/// The command to run
#[command(subcommand)]
pub command: Command,
/// Set when to use color.
/// auto = use color if a capable terminal is detected
#[clap(
long,
value_name = "WHEN",
require_equals = true,
num_args = 0..=1,
default_value = "auto",
default_missing_value = "always",
)]
pub color: ColorChoice,
/// Path to a custom CA certificate to use when making network requests.
#[clap(long = "cert", env = "TYPST_CERT")]
pub cert: Option<PathBuf>,
}
#[hoge]はattributeでメタデータに関するものなので今回は無視。 &ARGS.command
にmatch構文が適用されていて、pub command: Command
と宣言されているのでCommand
型を見てくるとする。
pub enum Command {
/// Compiles an input file into a supported output format
#[command(visible_alias = "c")]
Compile(CompileCommand),
/// Watches an input file and recompiles on changes
#[command(visible_alias = "w")]
Watch(CompileCommand),
/// Initializes a new project from a template
Init(InitCommand),
/// Processes an input file to extract provided metadata
Query(QueryCommand),
/// Lists all discovered fonts in system and custom font paths
Fonts(FontsCommand),
/// Self update the Typst CLI
#[cfg_attr(not(feature = "self-update"), clap(hide = true))]
Update(UpdateCommand),
}
また関数宣言かよ 愚痴っても仕方ない、CompileCommand
を見にいく。
pub struct CompileCommand {
/// Shared arguments
pub common: SharedArgs,
/// Path to output file (PDF, PNG or SVG). Use `-` to write output to stdout.
pub output: Option<Output>,
pub pages: Option<Vec<PageRangeArgument>>,
/// Output a Makefile rule describing the current compilation
pub make_deps: Option<PathBuf>,
/// The format of the output file, inferred from the extension by default
pub format: Option<OutputFormat>,
/// Opens the output file with the default viewer or a specific program after compilation
pub open: Option<Option<String>>,
/// The PPI (pixels per inch) to use for PNG export
pub ppi: f32,
/// Produces performance timings of the compilation process (experimental)
pub timings: Option<Option<PathBuf>>,
/// One (or multiple comma-separated) PDF standards that Typst will enforce conformance with.
pub pdf_standard: Vec<PdfStandard>,
}
(長かったためコメントとメタデータを一部切り取り)
とてもそれっぽいものに遭遇した。 しかしTypstのコードは注釈も多くて読みやすい。今回はSharedArgs
以下にありそう。
pub struct SharedArgs {
/// Path to input Typst file. Use `-` to read input from stdin
pub input: Input,
/// Configures the project root (for absolute paths)
pub root: Option<PathBuf>,
/// Add a string key-value pair visible through `sys.inputs`
pub inputs: Vec<(String, String)>,
/// Common font arguments
pub font_args: FontArgs,
/// The document's creation date formatted as a UNIX timestamp.
pub creation_timestamp: Option<DateTime<Utc>>,
/// The format to emit diagnostics in
pub diagnostic_format: DiagnosticFormat,
/// Arguments related to storage of packages in the system
pub package_storage_args: PackageStorageArgs,
/// Number of parallel jobs spawned during compilation, defaults to number of CPUs. Setting it to 1 disables parallelism.
pub jobs: Option<usize>,
}
と、ここまで入力を探ってて今気づいたのだが、
別に入力の処理追っても#image
関数の動作と関係なくないか???
ということで戻ってサクっとmatch文の先に行くとしましょう(45minロス)
fn dispatch() -> HintedStrResult<()> {
let timer = Timer::new(&ARGS);
match &ARGS.command {
Command::Compile(command) => crate::compile::compile(timer, command.clone())?,
Command::Watch(command) => crate::watch::watch(timer, command.clone())?,
Command::Init(command) => crate::init::init(command)?,
Command::Query(command) => crate::query::query(command)?,
Command::Fonts(command) => crate::fonts::fonts(command),
Command::Update(command) => crate::update::update(command)?,
}
Ok(())
}
今回はCommand::Compile(command)
にmatchするので crate::compile::compile(timer, command.clone())
が走るはず。(ちなみに後ろに付いてる?
はErr
を受けとったときに関数の返り値をErr
とする すげぇ)。
pub struct CompileCommand {
/// Shared arguments
#[clap(flatten)]
pub common: SharedArgs,
...
あれ?
と思ったがcommand.clone()
が先に走ったからこちらに潜ることとなったらしい。ということでとっとと抜けだしてもう一度潜ってみる。
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
// Only meant for input validation
_ = command.output_format()?;
let mut world =
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
Ok(())
}
それっぽい! とりあえずoutput_format()
でOutput
型をさわっているのでその確認をしにいく。
pub enum Output {
/// Stdout, represented by `-`.
Stdout,
/// A non-empty path.
Path(PathBuf),
}
出力先をstdout
にするかPath(pdf他)
にするだけっぽい。 とりあえずスルーして先に行けそう。
let mut world = SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
なかなかに圧のある文章だが、 SystemWorld型以下に設定されているnew関数にcommand.common
を渡して、失敗したらエラー処理のように読める(多分) なのでとりあえずSystemWorld型とnew関数を見にいこう。
/// A world that provides access to the operating system.
pub struct SystemWorld {
/// The working directory.
workdir: Option<PathBuf>,
/// The root relative to which absolute paths are resolved.
root: PathBuf,
/// The input path.
main: FileId,
/// Typst's standard library.
library: LazyHash<Library>,
/// Metadata about discovered fonts.
book: LazyHash<FontBook>,
/// Locations of and storage for lazily loaded fonts.
fonts: Vec<FontSlot>,
/// Maps file ids to source files and buffers.
slots: Mutex<HashMap<FileId, FileSlot>>,
/// Holds information about where packages are stored.
package_storage: PackageStorage,
/// The current datetime if requested. This is stored here to ensure it is
/// always the same within one compilation.
/// Reset between compilations if not [`Now::Fixed`].
now: Now,
/// The export cache, used for caching output files in `typst watch`
/// sessions.
export_cache: ExportCache,
}
pub fn new(command: &SharedArgs) -> Result<Self, WorldCreationError> {
// Set up the thread pool.
if let Some(jobs) = command.jobs {
rayon::ThreadPoolBuilder::new()
.num_threads(jobs)
.use_current_thread()
.build_global()
.ok();
}
// Resolve the system-global input path.
let input = match &command.input {
Input::Stdin => None,
Input::Path(path) => {
Some(path.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::InputNotFound(path.clone())
}
_ => WorldCreationError::Io(err),
})?)
}
};
// Resolve the system-global root directory.
let root = {
let path = command
.root
.as_deref()
.or_else(|| input.as_deref().and_then(|i| i.parent()))
.unwrap_or(Path::new("."));
path.canonicalize().map_err(|err| match err.kind() {
io::ErrorKind::NotFound => {
WorldCreationError::RootNotFound(path.to_path_buf())
}
_ => WorldCreationError::Io(err),
})?
};
let main = if let Some(path) = &input {
// Resolve the virtual path of the main file within the project root.
let main_path = VirtualPath::within_root(path, &root)
.ok_or(WorldCreationError::InputOutsideRoot)?;
FileId::new(None, main_path)
} else {
// Return the special id of STDIN otherwise
*STDIN_ID
};
let library = {
// Convert the input pairs to a dictionary.
let inputs: Dict = command
.inputs
.iter()
.map(|(k, v)| (k.as_str().into(), v.as_str().into_value()))
.collect();
Library::builder().with_inputs(inputs).build()
};
let fonts = Fonts::searcher()
.include_system_fonts(!command.font_args.ignore_system_fonts)
.search_with(&command.font_args.font_paths);
let now = match command.creation_timestamp {
Some(time) => Now::Fixed(time),
None => Now::System(OnceLock::new()),
};
Ok(Self {
workdir: std::env::current_dir().ok(),
root,
main,
library: LazyHash::new(library),
book: LazyHash::new(fonts.book),
fonts: fonts.fonts,
slots: Mutex::new(HashMap::new()),
package_storage: package::storage(&command.package_storage_args),
now,
export_cache: ExportCache::new(),
})
}
...
5日目
【悲報】main.rs から追ってたら時間がたりない!
【悲報2】#rotate functionを見付けました!
……どーすんだこれ
と、いってても仕方ないので一つずつ対処していきましょう。
main.rsからでは時間が足りない
そのためのファイル全探索。image
で検索をかけてみる。
あっ!(ターン)
いた。
とりあえずこっちは解決ということで、もうひとつの問題に目をむけなけねば。
rotate functionがあった
いやまぁ、 ありますよね……
さてどうしましょうか、というところでこんなことに気付いた。
ある画像を回転しながらcaptionをつけるという、いかにもやりそうなことをしてみる。
#figure(
rotate(90deg, image("not_desired_behavior.png")),
caption: [ Not desired behavior ],
)
するとこうなる。
not_desired_behavior
あれ?
どうもリサイズが自動で効かないようである。 また、#figure
や#rotate
のサイズの最適化オプションもうまくいかないようだ。
ということで、ここからは開発目標を
#figure
引数内で呼びだされた画像でも形を枠内でおさめる
ことにする。
ということで、image/
以下を探っていくことにするが、それはまた次回の話(進捗ダメです)
6日目
今更(?)であるが、Typstのコンパイルの道筋を確認しよう
//! The compiler for the _Typst_ markup language.
//!
//! # Steps
//! - **Parsing:**
//! The compiler first transforms a plain string into an iterator of [tokens].
//! This token stream is [parsed] into a [syntax tree]. The tree itself is
//! untyped, but the [AST] module provides a typed layer over it.
//! - **Evaluation:**
//! The next step is to [evaluate] the markup. This produces a [module],
//! consisting of a scope of values that were exported by the code and
//! [content], a hierarchical, styled representation of what was written in
//! the source file. The elements of the content tree are well structured and
//! order-independent and thus much better suited for further processing than
//! the raw markup.
//! - **Layouting:**
//! Next, the content is [layouted] into a [document] containing one [frame]
//! per page with items at fixed positions.
//! - **Exporting:**
//! These frames can finally be exported into an output format (currently PDF,
//! PNG, or SVG).
//!
//! [tokens]: syntax::SyntaxKind
//! [parsed]: syntax::parse
//! [syntax tree]: syntax::SyntaxNode
//! [AST]: syntax::ast
//! [evaluate]: eval::eval
//! [module]: foundations::Module
//! [content]: foundations::Content
//! [layouted]: crate::layout::layout_document
//! [document]: model::Document
//! [frame]: layout::Frame
つまり、今回の問題はEvaluation と Layoutの辺りでおこっていると考えられる。
参照するためにrotate
関数の中身を覗こう。
/// Rotates content without affecting layout.
///
/// Rotates an element by a given angle. The layout will act as if the element
/// was not rotated unless you specify `{reflow: true}`.
///
/// # Example
/// ```example
/// #stack(
/// dir: ltr,
/// spacing: 1fr,
/// ..range(16)
/// .map(i => rotate(24deg * i)[X]),
/// )
/// ```
#[elem(Show)]
pub struct RotateElem {
/// The amount of rotation.
///
/// ```example
/// #rotate(-1.571rad)[Space!]
/// ```
///
#[positional]
pub angle: Angle,
/// The origin of the rotation.
///
/// If, for instance, you wanted the bottom left corner of the rotated
/// element to stay aligned with the baseline, you would set it to `bottom +
/// left` instead.
///
/// ```example
/// #set text(spacing: 8pt)
/// #let square = square.with(width: 8pt)
///
/// #box(square())
/// #box(rotate(30deg, origin: center, square()))
/// #box(rotate(30deg, origin: top + left, square()))
/// #box(rotate(30deg, origin: bottom + right, square()))
/// ```
#[fold]
#[default(HAlignment::Center + VAlignment::Horizon)]
pub origin: Alignment,
/// Whether the rotation impacts the layout.
///
/// If set to `{false}`, the rotated content will retain the bounding box of
/// the original content. If set to `{true}`, the bounding box will take the
/// rotation of the content into account and adjust the layout accordingly.
///
/// ```example
/// Hello #rotate(90deg, reflow: true)[World]!
/// ```
#[default(false)]
pub reflow: bool,
/// The content to rotate.
#[required]
pub body: Content,
}
impl Show for Packed<RotateElem> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(BlockElem::single_layouter(self.clone(), layout_rotate)
.pack()
.spanned(self.span()))
}
}
/// Layout the rotated content.
#[typst_macros::time(span = elem.span())]
fn layout_rotate(
elem: &Packed<RotateElem>,
engine: &mut Engine,
locator: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let angle = elem.angle(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = if region.size.is_finite() {
compute_bounding_box(region.size, Transform::rotate(-angle)).1
} else {
Size::splat(Abs::inf())
};
measure_and_layout(
engine,
locator,
region,
size,
styles,
elem.body(),
Transform::rotate(angle),
align,
elem.reflow(styles),
)
}
返り値にあたるmeasure_and_layout
がframe
型をもっているをもっているので、ここがどういう挙動をしているかを見てみたい。
fn measure_and_layout(
engine: &mut Engine,
locator: Locator,
region: Region,
size: Size,
styles: StyleChain,
body: &Content,
transform: Transform,
align: Axes<FixedAlignment>,
reflow: bool,
) -> SourceResult<Frame> {
if reflow {
// Measure the size of the body.
let pod = Region::new(size, Axes::splat(false));
let frame = layout_frame(engine, body, locator.relayout(), styles, pod)?;
// Actually perform the layout.
let pod = Region::new(frame.size(), Axes::splat(true));
let mut frame = layout_frame(engine, body, locator, styles, pod)?;
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Compute the bounding box and offset and wrap in a new frame.
let (offset, size) = compute_bounding_box(frame.size(), ts);
frame.transform(ts);
frame.translate(offset);
frame.set_size(size);
Ok(frame)
} else {
// Layout the body.
let mut frame = layout_frame(engine, body, locator, styles, region)?;
let Axes { x, y } = align.zip_map(frame.size(), FixedAlignment::position);
// Compute the transform.
let ts = Transform::translate(x, y)
.pre_concat(transform)
.pre_concat(Transform::translate(-x, -y));
// Apply the transform.
frame.transform(ts);
Ok(frame)
}
}
reflow
が重要な分岐を任されているっぽい。 あれ、でもそんな変数をみた記憶が……
あっ
いた。 しかもなんか重要なこと書いてある。 なんか猛烈に嫌な予感がする……
#figure(
rotate(90deg, image("not_desired_behavior.png"), reflow: true),
caption: [ Not desired behavior ],
)
あっ
できちゃった
……
気をとりなおそう……
目標変更
とりあえず#rotate
関数は思ったより優秀だった。 そして私達が考えることぐらい先に誰かがやっていることを痛感した そりゃそうじゃ
よってこれからは
#image関数
からでもrotateできるようにする
を新たな目標とする。
そのためには
- フレームワークの設定
- 回転をどうやっているか
を見ていこう。
なんかできちゃった
とりあえず先日みつけたimage
以下mod.rs
を眺めてみる。
pub struct ImageElem {
/// Path to an image file
///
/// For more details, see the [Paths section]($syntax/#paths).
#[required]
#[parse(
let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
path
)]
#[borrowed]
pub path: EcoString,
/// The raw file data.
#[internal]
#[required]
#[parse(Readable::Bytes(data))]
pub data: Readable,
/// The image's format. Detected automatically by default.
///
/// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image
/// is [not currently supported](https://github.com/typst/typst/issues/145).
pub format: Smart<ImageFormat>,
/// The width of the image.
pub width: Smart<Rel<Length>>,
/// The height of the image.
pub height: Sizing,
/// A text describing the image.
pub alt: Option<EcoString>,
/// How the image should adjust itself to a given area (the area is defined
/// by the `width` and `height` fields). Note that `fit` doesn't visually
/// change anything if the area's aspect ratio is the same as the image's
/// one.
///
/// ```example
/// #set page(width: 300pt, height: 50pt, margin: 10pt)
/// #image("tiger.jpg", width: 100%, fit: "cover")
/// #image("tiger.jpg", width: 100%, fit: "contain")
/// #image("tiger.jpg", width: 100%, fit: "stretch")
/// ```
#[default(ImageFit::Cover)]
pub fit: ImageFit,
}
これは#rotate関数の引数と合致しているので、どうやらここで型定義していそうである。
そのままimage/
以下を探していると、rotate
という文字がraster.rs
以下にいた。
/// Apply an EXIF rotation to a dynamic image.
fn apply_rotation(image: &mut DynamicImage, rotation: u32) {
use image::imageops as ops;
match rotation {
2 => ops::flip_horizontal_in_place(image),
3 => ops::rotate180_in_place(image),
4 => ops::flip_vertical_in_place(image),
5 => {
ops::flip_horizontal_in_place(image);
*image = image.rotate270();
}
6 => *image = image.rotate90(),
7 => {
ops::flip_horizontal_in_place(image);
*image = image.rotate90();
}
8 => *image = image.rotate270(),
_ => {}
}
}
なんだこれ? てかEXIFてなんだ……
どうやらEXIFというのは、写真自体がもっている90°回転やら鏡像やらのメタデータらしい。そしてこの関数はそのEXIF関数を使って写真を回転しているらしい。
……これ、使えないか?
90°回転や鏡像に限られてはいるけどレポートで使うのは精々そのぐらいだろうし、コンパイルの流れからもLayout
より前の[content]
生成の段階で画像に作用できるので、フレームワークの問題に衝突することもないだろう。かなりよさそうでは?
とりあえずやってみよう。まずはImageElem
がどこで作られているかコールスタックをみていく。するとこんなものにたどりつく。
impl Element {
…
/// Construct an instance of this element.
pub fn construct(
self,
engine: &mut Engine,
args: &mut Args,
) -> SourceResult<Content> {
(self.0.construct)(engine, args)
}
…
}
ここでImageElem
を作っているようだ。 関数がself
をとっているからstruct ImageElem
の中で型を完成させているらしい。 よってとりあえずImageElem
を変更していく。
型定義をしているmod.rs
に他のものに倣ってrotation
という引数を追加してみよう。 具体的にはこう↓
pub struct ImageElem {
#[required]
#[parse(
let Spanned { v: path, span } =
args.expect::<Spanned<EcoString>>("path to image file")?;
let id = span.resolve_path(&path).at(span)?;
let data = engine.world.file(id).at(span)?;
path
)]
#[borrowed]
pub path: EcoString,
#[internal]
#[required]
#[parse(Readable::Bytes(data))]
pub data: Readable,
pub format: Smart<ImageFormat>,
pub width: Smart<Rel<Length>>,
pub height: Sizing,
pub alt: Option<EcoString>,
#[default(ImageFit::Cover)]
pub fit: ImageFit,
+ #[default(1)] // FIXME: Added
+ pub rotate: u32, // FIXME: Added
}
テストファイルはこのとおり
#figure(
image("not_desired_behavior.png", rotate: 5),
caption: "Not desired behavior"
)
するとコンパイルしてもエラーを吐かなかった。
次に、このImageElem
型を処理している関数を探す。するとすぐ下にlayout_image
がみつかる。
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
…
}
ここで、各ImageElem
の要素がelem.format(styles)
のように呼びだされていることがわかる。そこで、確認のためprintln!("Rotate: {:?}", elem.rotate(styles));
としてrotate
が認識されているか確認する。すると、
Rotate: 5
という文面が出たため、ちゃんと認識されていることがわかる。
さて、あとはこの引数をどうやってapply_rotation
に渡すかだが、これのヒントとなったのがraster.rs
以下のRasterImage::new
にいた
…
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
apply_rotation(&mut dynamic, rotation);
}
…
どうもRasterImage::new
のタイミングで渡せばいいらしい。 あとはnew
がどこで呼ばれるかを見にいこう。
impl Image {
pub fn new(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(data, format)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::new(data)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
pub fn with_fonts(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
world: Tracked<dyn World + '_>,
families: &[&str],
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(data, format)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
}
いた。 ここで型がImage
になっているのでどこかでImage
型をつくっているはずである。それは先程の
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
…
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
)
.at(span)?;
…
}
で作られているのがわかる。
あとは、それぞれにrotate
引数を渡してみて、うまくいくかを確認する。
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
+ elem.rotate(styles),
)
.at(span)?;
impl Image {
pub fn new(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
- ImageKind::Raster(RasterImage::new(data, format)?)
+ ImageKind::Raster(RasterImage::new(data, format, 1)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::new(data)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
pub fn with_fonts(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
world: Tracked<dyn World + '_>,
families: &[&str],
+ rotation: u32,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
- ImageKind::Raster(RasterImage::new(data, format)?)
+ ImageKind::Raster(RasterImage::new(data, format, rotation)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
}
impl RasterImage {
/// Decode a raster image.
#[comemo::memoize]
- pub fn new(data: Bytes, format: RasterFormat) -> StrResult<RasterImage> {
+ pub fn new(data: Bytes, format: RasterFormat, rotation: u32) -> StrResult<RasterImage> {
…
let exif = exif::Reader::new()
.read_from_container(&mut std::io::Cursor::new(&data))
.ok();
+ apply_rotation(&mut dynamic, rotation);
// Apply rotation from EXIF metadata.
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
apply_rotation(&mut dynamic, rotation);
}
// Extract pixel density.
let dpi = determine_dpi(&data, exif.as_ref());
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
}
}
さて、どうなるか。
やったーーー!
7日目
とりあえずできたものはできたので、バグ潰し。
EXIFデータで既に回転が指定されているときに予想外の挙動をする。
これは割と簡単。 関数側の回転をEXIFデータによる回転の後においてあげる。
impl RasterImage {
/// Decode a raster image.
#[comemo::memoize]
pub fn new(data: Bytes, format: RasterFormat, rotation: u32) -> StrResult<RasterImage> {
…
let exif = exif::Reader::new()
.read_from_container(&mut std::io::Cursor::new(&data))
.ok();
- apply_rotation(&mut dynamic, rotation);
// Apply rotation from EXIF metadata.
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
apply_rotation(&mut dynamic, rotation);
}
+ apply_rotation(&mut dynamic, rotation);
// Extract pixel density.
let dpi = determine_dpi(&data, exif.as_ref());
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
}
}
引数名が重複している
引数がシャドーイングしていて気付いていなかった。(これrust的には便利な機能らしいのだが、まだあまりよさがわからない)
impl RasterImage {
/// Decode a raster image.
#[comemo::memoize]
- pub fn new(data: Bytes, format: RasterFormat, rotation: u32) -> StrResult<RasterImage> {
+ pub fn new(data: Bytes, format: RasterFormat, my_rotation: u32) -> StrResult<RasterImage> {
…
let exif = exif::Reader::new()
.read_from_container(&mut std::io::Cursor::new(&data))
.ok();
// Apply rotation from EXIF metadata.
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { // ここで引数が衝突してた
apply_rotation(&mut dynamic, rotation);
}
- apply_rotation(&mut dynamic, rotation);
+ apply_rotation(&mut dynamic, my_rotation);
// Extract pixel density.
let dpi = determine_dpi(&data, exif.as_ref());
Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi })))
}
}
とりあえず以上。 これからの方針として
- EXIFデータはu32で直感的理解がし辛いのでAngleとmirrorという引数にわける
- vector imageに対するローテーションができないため、実装を考える
- その他Typstに不満な点を探す
- スライドを作る
8日目
Angleの実装
そもそもAngle
という型がTypstには用意されているので、それを見にいく。
impl Angle {
/// The zero angle.
pub const fn zero() -> Self {
Self(Scalar::ZERO)
}
/// Create an angle from a number of raw units.
pub const fn raw(raw: f64) -> Self {
Self(Scalar::new(raw))
}
……
}
色々便利そうなので流用させてもらおう。 Image
構造体の定義のところを書きかえていく
+ use crate::layout::Angle;
…
pub struct ImageElem {
…
- #[default(1)]
- pub rotate: u32,
+ #[default(Angle::zero())]
+ pub rotate: Angle
}
…
impl Image {
…
pub fn new(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
- ImageKind::Raster(RasterImage::new(data, format, 1)?)
+ ImageKind::Raster(RasterImage::new(data, format, Angle::zero())?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::new(data)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
pub fn with_fonts(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
world: Tracked<dyn World + '_>,
families: &[&str],
- rotation: u32,
+ rotation: Angle,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(data, format, rotation)?)
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
}
+ use crate::layout::Angle;
impl RasterImage {
/// Decode a raster image.
#[comemo::memoize]
pub fn new(
data: Bytes,
format: RasterFormat,
+ my_rotation: Angle,
) -> StrResult<RasterImage> {
…
}
}
+ fn apply_user_rotation(image: &mut DynamicImage, rotation: Angle) {
+ use image::imageops as ops;
+ let rotation = rotation.to_deg() as u32;
+ match rotation {
+ 90 => *image = image.rotate90(),
+ 180 => ops::rotate180_in_place(image),
+ 270 => *image = image.rotate270(),
+ _ => {}
+ }
+}
と、すれば実装できる。
mirrorの実装
先程とあまり変らない、とおもって構造体を実装するとこのようなエラーにぶつかった。
pub struct ImageElem {
…
#[default(Angle::zero())]
pub rotate: Angle
+ pub mirrot: &str // ■ missing lifetime specifier expected named lifetime parameter
}
ほーん?
しかし実際Typst
内で&str
が呼ばれてるのをみたことは少ない。むしろ(謎)のEcoString
という型がよく使われてる。ということで型をEcoString
にしてあげると(なぜか)上手くいったため、以降そのように実装する。いつかライフタイムについては理解しよう。
(余談だが、ImageElemは構造体要素に対してOption<T>を自動的に付けるため、ここでOptionを付ける必要はない(一敗))
pub struct ImageElem {
…
#[default(Angle::zero())]
pub rotate: Angle
pub mirrot: EcoString
}
…
fn layout_image(
elem: &Packed<ImageElem>,
engine: &mut Engine,
_: Locator,
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
…
let image = Image::with_fonts(
data.clone().into(),
format,
elem.alt(styles),
engine.world,
&families(styles).collect::<Vec<_>>(),
elem.rotate(styles),
+ elem.mirror(styles),
)
.at(span)?;
}
impl Image {
…
pub fn new(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
ImageKind::Raster(RasterImage::new(
data,
format,
Angle::zero(),
+ EcoString::new(),
)?)
// FIXME: temporary implementation
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::new(data)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
…
pub fn with_fonts(
data: Bytes,
format: ImageFormat,
alt: Option<EcoString>,
world: Tracked<dyn World + '_>,
families: &[&str],
rotation: Angle,
+ mirror: EcoString,
) -> StrResult<Image> {
let kind = match format {
ImageFormat::Raster(format) => {
- ImageKind::Raster(RasterImage::new(data, format, rotation)?)
+ ImageKind::Raster(RasterImage::new(data, format, rotation, mirror)?)
//FIXME: added rotation and mirror
}
ImageFormat::Vector(VectorFormat::Svg) => {
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
}
};
Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt }))))
}
impl RasterImage {
/// Decode a raster image.
#[comemo::memoize]
pub fn new(
data: Bytes,
format: RasterFormat,
my_rotation: Angle,
+ my_mirror: EcoString,
) -> StrResult<RasterImage> {
…
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
apply_rotation(&mut dynamic, rotation);
}
apply_user_rotation(&mut dynamic, my_rotation);
+ apply_user_mirror(&mut dynamic, my_mirror);
…
}
}
+fn apply_user_mirror(image: &mut DynamicImage, mirror: EcoString) {
+ use image::imageops as ops;
+ let mirror = mirror.as_str();
+ match mirror {
+ "horizontal" => {
+ println!("mirror_horizontal");
+ ops::flip_horizontal_in_place(image);
+ }
+ "vertical" => {
+ println!("mirror_vertical");
+ ops::flip_vertical_in_place(image);
+ }
+ _ => {
+ println!("no mirror");
+ }
+ }
}
これでうまくいった。 割とシンプル。
バグをみつけました!!!
しかも結構やっかいなやつ。 とりあえず以下をみてほしい。
#figure(
image("not_desired_behavior.png"),
caption: [
Example Image
],
)
#figure(
image("not_desired_behavior.png", rotate: 90deg),
caption: [
Example Image
],
)
結果
Huh?
なんじゃこりゃ。
検証
これより前に、
#image("not_desired_behavior.png", mirror: "")
#image("not_desired_behavior.png", mirror: "horizontal")
としても、
みて!蟹がおどってるよ!
蟹の方向がかわらないというバグに遭遇はしていた。 上のバグがみつかるまでは実装の問題かなーと思っていたが、どうやら違いそうだとわかった。
つまり、今回おこった(らしき)現象は
- 画像aの設定を読みこむ(ここまでは正常)
- 画像aの設定を再び読みこむとき、画像自体に回転やら反転はおこらないが、フレームのみに適応される
- そのままフレームにはまるように画像が適応され、歪んだりする
ということらしい。 なんでやねん。
9日目
バグ取りはあきらめスライド作成へ勤しんだ...