Open8

Typstに機能追加してみたい! (大規模OSSを手探る-進捗ブログ)

HTsulfuricHTsulfuric

2日目

なにをやるか

  • ソフトウェア: Typst
  • やりたいこと: LaTeXの\includegraphicsにはangleオプションがあるが、typstのimage()関数にはangleに類するオプションが無いため、その実装をめざしたい。え? angleオプション使ったことないって? 知らん

buildまで

とりあえずgithubからTypstをクローンし開発用のレポジトリ(プライベートだが)におく。
https://github.com/typst/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

HTsulfuricHTsulfuric

3日目

rustbookを読むだけの一日となった。第7章まで読んだので次回からはデバッギングに移行したい。

Rust:
Good for safety, need a PHD to print Hello World

HTsulfuricHTsulfuric

4日目

デバッグ環境

CodeLLDB + Rustを使用。./.vscode/launch.JSON以下に既に色々デバッグやユニットテストがあったが、今回は以下を追記した。

./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"],
}

とりあえずわかったこと

typst-cli/src/main.rs
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()内でおこってる。

typst-cli/main.rs
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に変数を渡さずに走らせたときにでるヘルプとまったく同じ。
typst_1
ほなこのMatchでコマンドみてるんやろ

じゃあ渡されているARGSは何だろうと思って、型のCliArgumentを見にいく

typst-cli/src/args.rs
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型を見てくるとする。

typst-cli/src/args.rs
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を見にいく。

typst-cli/src/args.rs
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以下にありそう。

typst-cli/src/args.rs
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ロス)

typst-cli/main.rs (再掲)
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とする すげぇ)。

typst-cli/src/args.rs
pub struct CompileCommand {
    /// Shared arguments
    #[clap(flatten)]
    pub common: SharedArgs,

...

あれ?
と思ったがcommand.clone()が先に走ったからこちらに潜ることとなったらしい。ということでとっとと抜けだしてもう一度潜ってみる。

typst-cli/src/compile.rs
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型をさわっているのでその確認をしにいく。

typst-cli/src/args.rs
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関数を見にいこう。

typst-cli/src/World.rs
/// 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(),
        })
    }


...
HTsulfuricHTsulfuric

5日目

【悲報】main.rs から追ってたら時間がたりない!

【悲報2】#rotate functionを見付けました!

……どーすんだこれ

と、いってても仕方ないので一つずつ対処していきましょう。

main.rsからでは時間が足りない

そのためのファイル全探索。imageで検索をかけてみる。

image_func
あっ!(ターン)

いた。

とりあえずこっちは解決ということで、もうひとつの問題に目をむけなけねば。

rotate functionがあった

いやまぁ、 ありますよね……

https://typst.app/docs/reference/layout/rotate/

さてどうしましょうか、というところでこんなことに気付いた。

ある画像を回転しながらcaptionをつけるという、いかにもやりそうなことをしてみる。

test.typ
#figure(
  rotate(90deg, image("not_desired_behavior.png")),
  caption: [ Not desired behavior ],
)

するとこうなる。

crab
not_desired_behavior

あれ?

どうもリサイズが自動で効かないようである。 また、#figure#rotateのサイズの最適化オプションもうまくいかないようだ。

ということで、ここからは開発目標を

#figure引数内で呼びだされた画像でも形を枠内でおさめる

ことにする。

ということで、image/以下を探っていくことにするが、それはまた次回の話(進捗ダメです)

HTsulfuricHTsulfuric

6日目

今更(?)であるが、Typstのコンパイルの道筋を確認しよう

typst/src/lib.rs
//! 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関数の中身を覗こう。

typst/src/layout/transform.rs
/// 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_layoutframe型をもっているをもっているので、ここがどういう挙動をしているかを見てみたい。

typst/src/layout/transorm.rs
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が重要な分岐を任されているっぽい。 あれ、でもそんな変数をみた記憶が……

reflow
あっ

いた。 しかもなんか重要なこと書いてある。 なんか猛烈に嫌な予感がする……

test.typ
#figure(
  rotate(90deg, image("not_desired_behavior.png"), reflow: true),
  caption: [ Not desired behavior ],
)

desired_behavior
あっ

できちゃった

……

気をとりなおそう……

目標変更

とりあえず#rotate関数は思ったより優秀だった。 そして私達が考えることぐらい先に誰かがやっていることを痛感した そりゃそうじゃ

よってこれからは
#image関数からでもrotateできるようにする

を新たな目標とする。

そのためには

  • フレームワークの設定
  • 回転をどうやっているか

を見ていこう。

なんかできちゃった

とりあえず先日みつけたimage以下mod.rsを眺めてみる。

typst/src/visualize/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以下にいた。

typst/src/visualize/image/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てなんだ……

https://ja.wikipedia.org/wiki/Exchangeable_image_file_format

どうやらEXIFというのは、写真自体がもっている90°回転やら鏡像やらのメタデータらしい。そしてこの関数はそのEXIF関数を使って写真を回転しているらしい。

……これ、使えないか?

90°回転や鏡像に限られてはいるけどレポートで使うのは精々そのぐらいだろうし、コンパイルの流れからもLayoutより前の[content]生成の段階で画像に作用できるので、フレームワークの問題に衝突することもないだろう。かなりよさそうでは?

とりあえずやってみよう。まずはImageElemがどこで作られているかコールスタックをみていく。するとこんなものにたどりつく。

typst/src/foundations/element.rs
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という引数を追加してみよう。 具体的にはこう↓

typst/src/visualize/image/mod.rs
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
}

テストファイルはこのとおり

test.typ
#figure(
    image("not_desired_behavior.png", rotate: 5),
    caption: "Not desired behavior"
)

するとコンパイルしてもエラーを吐かなかった。

次に、このImageElem型を処理している関数を探す。するとすぐ下にlayout_imageがみつかる。

typst/src/visualize/image/mod.rs
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にいた

typst/src/visualize/image/raster.rs
if let Some(rotation) = exif.as_ref().and_then(exif_rotation) {
            apply_rotation(&mut dynamic, rotation);
        }

どうもRasterImage::newのタイミングで渡せばいいらしい。 あとはnewがどこで呼ばれるかを見にいこう。

typst/rc/visualize/image/mod.rs
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型をつくっているはずである。それは先程の

typst/rc/visualize/image/mod.rs
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引数を渡してみて、うまくいくかを確認する。

typst/rc/visualize/image/mod.rs
    let image = Image::with_fonts(
        data.clone().into(),
        format,
        elem.alt(styles),
        engine.world,
        &families(styles).collect::<Vec<_>>(),
+       elem.rotate(styles),
    )
    .at(span)?;
typst/rc/visualize/image/mod.rs
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 }))))
    }
}
typst/src/visualize/image/raster.rs
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 })))
    }
}

さて、どうなるか。

crab_crab

やったーーー!

HTsulfuricHTsulfuric

7日目

とりあえずできたものはできたので、バグ潰し。

EXIFデータで既に回転が指定されているときに予想外の挙動をする。

これは割と簡単。 関数側の回転をEXIFデータによる回転の後においてあげる。

typst/src/visualize/image/raster.rs
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的には便利な機能らしいのだが、まだあまりよさがわからない)

typst/src/visualize/image/raster.rs
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に不満な点を探す
  • スライドを作る
HTsulfuricHTsulfuric

8日目

Angleの実装

そもそもAngleという型がTypstには用意されているので、それを見にいく。

typst/src/layout/angle.rs
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構造体の定義のところを書きかえていく

typst/rc/visualize/image/mod.rs
+ 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 }))))
    }
}
typst/rc/visualize/image/raster.rs
+ 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の実装

先程とあまり変らない、とおもって構造体を実装するとこのようなエラーにぶつかった。

typst/rc/visualize/image/mod.rs

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を付ける必要はない(一敗))

typst/rc/visualize/image/mod.rs

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 }))))
    }
typst/rc/visualize/image/raster.rs
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");
+        }
+    }
}

これでうまくいった。 割とシンプル。

バグをみつけました!!!

しかも結構やっかいなやつ。 とりあえず以下をみてほしい。

test.typ

#figure(
    image("not_desired_behavior.png"),
    caption: [
        Example Image
    ],
)

#figure(
    image("not_desired_behavior.png", rotate: 90deg),
    caption: [
        Example Image
    ],
)

結果

what_the
Huh?

なんじゃこりゃ。

検証

これより前に、

test
#image("not_desired_behavior.png", mirror: "")

#image("not_desired_behavior.png", mirror: "horizontal")

としても、
kani_kani
みて!蟹がおどってるよ!

蟹の方向がかわらないというバグに遭遇はしていた。 上のバグがみつかるまでは実装の問題かなーと思っていたが、どうやら違いそうだとわかった。

つまり、今回おこった(らしき)現象は

  1. 画像aの設定を読みこむ(ここまでは正常)
  2. 画像aの設定を再び読みこむとき、画像自体に回転やら反転はおこらないが、フレームのみに適応される
  3. そのままフレームにはまるように画像が適応され、歪んだりする

ということらしい。 なんでやねん。