目次
あらすじ
tl;dr 事情があって、Rust で漫画読みアプリを作ることになった。
体験談のところまで飛ばす
私自身は booth.pm や dlsite.com のヘビーユーザーですが、
そういうサイトで買った同人誌や漫画は専用ビューアじゃなくても開けるファイルが結構あります。
そこで、普段、画像ビューアなどのソフトで開いて読みますが、結論からいうと自分が欲しい機能が足りないことや描画速度が遅いソフトが多く、どちらにも満足できていません。
最初はどちらかの OpenSource のソフトから改造しようと思いましたが、
元々使っている言語の処理能力が足りなかったり、既に自分に見合ってない方向で作り込んでいたりしました。
まして時代はマルチプラットフォームだから、遅い Javascript などで作られていることが多く、
C++ など早い言語で作っているところは非常に少ないです。OpenSource ともなればそれはごく僅か。。。
また、Windows 環境を中心としている自分には向いていませんでした。
昔は MangaMeeya という非常に優秀な漫画読みアプリがありましたが、開発者は既に更新しなくなり、新しい画像のフォーマットに対応していませんでした。代わりのソフトをずっと探していましたが、どれも満足できず。。。
だったら、自分で作れば?ということになりました。

なぜ Rust
元々 C++ で作る予定でいましたが、開発者の皆様が知っている通り、かなり難易度が高いです。
メモリ管理が非常に手間で、String の使い方や、ライブラリの競合が激しいなどです。
なので、最近、巷で流行っている Rust を試そうかとずっと興味がありました。
それに加えて
・処理速度が早い
・C++より描きやすい(かも)
・ライブラリが豊富で Dependency 対応もしやすい
よって、Rust で作ることに決めました。
問題発覚
Rust って使ったことないですよねー。。。
でもずっと前から興味あったし、勉強もしながら作ってみようと思います。
AI に聞いてみた
勉強材料として、Gemini を使うことにしました。
最初は僕もバイブコーディングに対してわりと良くない印象をもっていましたが、
今はもうかけがえのない存在になりつつあります。
Gemini で 0 からバイブコーディングしてみた
とりあえず要件をまとめて、Gemini に投げることにしてみました。
最初から機能いっぱいで入れると混乱しそうなので、少しずつ入れることにしました。
プロンプトはこんな感じです。
Create a rust manga reader application for me.
The application will be solely exclusive for windows platform.
The first screen should be a blank, black screen, and when I press on any place on screen,
the windows file selection dialog should popup for file selection.
The application should support opening zip file. After file is selected for opening,
the application should detect whether it is a zip file containing jpg, png or not. If not, show an error message.
Otherwise, load the image in the zip file and show it on the screen.
A click on the image should show the next image in zip file.
When loading the image in zip file, the application should not extract the zip file, but instead open the file directly from the archive.
日本語版はこんな感じです。
Rustで漫画読みアプリを作成してください。
このアプリケーションは Windows プラットフォーム専用とします。
最初の画面は何も表示されない真っ黒な画面にし、画面上の任意の場所をクリックしたときに、ファイル選択のための Windows ファイル選択ダイアログがポップアップするようにしてください。
アプリケーションは ZIP ファイルの展開に対応させる必要があります。ZIP ファイルが選択された後、その ZIP ファイルの中に JPG や PNG が含まれているかどうかを検出してください。もし含まれていない場合は、エラーメッセージを表示してください。
画像が含まれている場合は、ZIP 内の最初の画像を読み込んで画面に表示してください。
画像をクリックすると、ZIP 内の次の画像が表示されるようにしてください。
ZIP 内の画像ファイルを読み込む際、アプリケーションは ZIP ファイルを解凍するのではなく、アーカイブから直接ファイルを開くようにしてください。
生成されたコードはこちら
Cargo.toml
# Cargo.toml
[package]
name = "manga_reader"
version = "0.1.0"
edition = "2021"
[dependencies]
eframe = "0.31" # Latest GUI framework
egui = "0.31"
rfd = "0.15" # Native file dialogs
zip = "2.2" # Zip file handling
image = "0.25" # Image decoding
main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // Hide console on release
use eframe::egui;
use std::fs::File;
use std::io::Read;
use std::path::PathBuf;
fn main() -> eframe::Result<()> {
// Set options for fullscreen
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_maximized(true)
.with_decorations(true), // Keep window controls (min/close)
..Default::default()
};
eframe::run_native(
"Zip Image Viewer",
options,
Box::new(|cc| Ok(Box::new(MyApp::new(cc)))),
)
}
struct MyApp {
// State storage
zip_path: Option<PathBuf>,
image_files: Vec<String>,
current_index: usize,
current_texture: Option<egui::TextureHandle>,
}
impl MyApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self {
zip_path: None,
image_files: Vec::new(),
current_index: 0,
current_texture: None,
}
}
// Helper to open file dialog and process zip
fn open_file_dialog(&mut self, ctx: &egui::Context) {
// Native Windows File Dialog
let file = rfd::FileDialog::new()
.add_filter("Zip Archive", &["zip"])
.add_filter("All Files", &["*"])
.set_title("Select a Zip file")
.pick_file();
if let Some(path) = file {
self.load_zip(path, ctx);
}
}
fn load_zip(&mut self, path: PathBuf, ctx: &egui::Context) {
let file = match File::open(&path) {
Ok(f) => f,
Err(_) => {
self.show_error("Could not open file.");
return;
}
};
let mut archive = match zip::ZipArchive::new(file) {
Ok(a) => a,
Err(_) => {
self.show_error("Selected file is not a valid zip archive.");
return;
}
};
// Scan for images
let mut images = Vec::new();
for i in 0..archive.len() {
if let Ok(file) = archive.by_index(i) {
let name = file.name().to_lowercase();
if name.ends_with(".png") || name.ends_with(".jpg") || name.ends_with(".jpeg") || name.ends_with(".bmp") {
images.push(file.name().to_string());
}
}
}
// Sort for consistent cycling order
images.sort();
if images.is_empty() {
self.show_error("The zip file contains no supported images.");
self.zip_path = None;
self.image_files.clear();
self.current_texture = None;
} else {
// Success: Reset state and load first image
self.zip_path = Some(path);
self.image_files = images;
self.current_index = 0;
self.load_current_image(ctx);
}
}
fn load_current_image(&mut self, ctx: &egui::Context) {
if let Some(path) = &self.zip_path {
if let Ok(file) = File::open(path) {
if let Ok(mut archive) = zip::ZipArchive::new(file) {
if let Some(filename) = self.image_files.get(self.current_index) {
if let Ok(mut zip_file) = archive.by_name(filename) {
let mut buffer = Vec::new();
if zip_file.read_to_end(&mut buffer).is_ok() {
// Load from memory using 'image' crate
if let Ok(image) = image::load_from_memory(&buffer) {
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
let color_image = egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
);
// Create a texture handle for egui
self.current_texture = Some(ctx.load_texture(
"displayed_image",
color_image,
egui::TextureOptions::LINEAR,
));
}
}
}
}
}
}
}
}
fn show_error(&self, message: &str) {
// Native Windows Error Dialog
rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title("Error")
.set_description(message)
.set_buttons(rfd::MessageButtons::Ok)
.show();
}
}
impl eframe::App for MyApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let central_panel = egui::CentralPanel::default();
central_panel.show(ctx, |ui| {
// Make the UI fill the entire screen
let available_size = ui.available_size();
if let Some(texture) = &self.current_texture {
// IMAGE VIEW MODE
// Show image centered and scaled to fit
let image = egui::Image::new(texture)
.fit_to_exact_size(available_size)
.maintain_aspect_ratio(true);
let image_response = ui.add(image);
// Logic: Click Image -> Next Image
if image_response.clicked() {
if !self.image_files.is_empty() {
self.current_index = (self.current_index + 1) % self.image_files.len();
self.load_current_image(ctx);
}
}
// Logic: Click Background (empty space around image) -> Open File Dialog
// We detect clicks on the generic UI background here
if ui.input(|i| i.pointer.any_click()) && !image_response.hovered() {
self.open_file_dialog(ctx);
}
} else {
// START MODE (Empty)
// Fill the screen with an invisible button to capture clicks anywhere
let rect = ui.available_rect_before_wrap();
let response = ui.allocate_rect(rect, egui::Sense::click());
// Center text
ui.allocate_ui_at_rect(rect, |ui| {
ui.centered_and_justified(|ui| {
ui.heading("Click anywhere to open a Zip file");
});
});
if response.clicked() {
self.open_file_dialog(ctx);
}
}
});
}
}
わるくない、、、わるくない!一発でコンパイルも通れた、、、Gemini 優秀だな。

と思いきや Dependency が古かったり、画面が何も反応しません。
ここまでできたら後は楽ですが、最初から完璧になって欲しいと思ったりしますね。
その後も色々機能追加したり、対応拡張子を増やしたり、2ページ読み対応だったりしていますが、
一週間にかけて、やっと完成しました。
完成した漫画アプリのコードはこちらになります。
https://github.com/Lycoris52/RustMangaReader

Gemini 超優秀だな!。。。と言いたいところですが、今回バイブコーディングするにあたり、かなり生成 AI の性能の限界を痛感しました。
時間が経つにつれてもっと賢くなると思いますが、現段階ではいくつかの壁にぶつかりました。
これは Gemini に限る問題ではなく、生成 AI 全般に言えるでしょう。
生成 AI が生成したコードの問題と対処法
課題その1:AI は知らないものを知らないと言わない
これは何回も遭遇しましたが、AI は「私は知らない、できない」とは言わないですね。。。
具体的な例を挙げますと、機能を追加する時の話ですが、
.rar という種類のアーカイブファイルを対応したいので、下記のプロンプトを作成しました。
このアプリは .rar ファイルも対応したい。
.zip ファイルを読み込むロジックに .rar ファイルも対応できるよう、
修正分のコードを作成してください。
生成されたコードはこちら
if let Ok(mut archive)= unrar::Archive::new(path).open_for_list() {
for entry in archive {
if let Ok(e) = entry {
let name = e.filename.to_lowercase();
if exts.iter().any(|&ext| name.ends_with(&format!(".{}", ext))) {
// Open rar for reading
if let Ok(mut read_archive)= unrar::Archive::new(path).open_for_data() {
if let Ok(mut data) = read_archive.read_data() {
// This is a simplified example; unrar usually requires
// iterating to the specific file or extracting to memory.
}
}
}
}
}
}
コンパイルが通らないのはともかく、なぜが非常に重要な部分がコメントだけになっています。
そこを問い詰めると全然コンパイルが通らないコードが生成されました。
その unrar のライブラリにない関数を使っていたり、コンパイルエラーがめちゃくちゃになってたりしています。
何回も修正の指示をしてもなかなか治らず、自分で全て修正することになりました。
let mut archive = unrar::Archive::new(path.to_str().unwrap()).open_for_processing().unwrap();
while let Ok(header) = archive.read_header() {
let filename = header.filename.clone();
// Check if it's a supported image
let is_image = valid_exts.iter().any(|&e| filename.to_lowercase().ends_with(e));
if is_image {
// read_data() returns the bytes and moves the archive to the next header
match header.read_data() {
Ok(data) => {
images.push((filename, data));
}
Err(_) => {
// If reading data fails, skip this file and try to continue
if let Ok(next) = header.skip_header() {
archive = next;
} else { break; }
continue;
}
}
} else {
// Not an image? Skip it to advance the internal pointer
if let Ok(next) = header.skip_header() {
archive = next;
} else { break; }
}
}
ライブラリの使い方を知らないなら「知らない」って言って欲しいですね。
ですが、どういう方向性で実装すればいいのかが分かったきたので、自分で修正しました。
対処法:
・参考のため、ライブラリのドキュメントリンクを付属する
・正しい関数を公式ドキュメントから探し出して、その関数を使うように指示する。
そうすると、生成されたコードが良くなりましたが、
何回も同じようなコードを見ていたので、方向性だけ掴めて自分で修正できるようになりました。
課題その2:コードベースが大きくなると元のコードを忘れがち
おそらく Context Size や Short Term Memory を節約するためだと考えられますが、Gemini の無料バーションの記憶力がかなり短くなっている気がします。
Pro のアカウントなら改善できるような気がしますが、無料アカウントはなんとも。。。
コードが 1000行超えるあたりから、生成 AI から生成されたコード怪しくなります。
・作ったことない関数の使用
・やっとことない工程やコードの参照
などが頻繁に起こります。
対処法:忘れた度にもう一度ソースコードを付属して、存在しないコードの部分を指摘すれば改正できます。
課題その3:不具合の対処法を聞いても同じコードが繰り返し生成される(そして、直らない)
下記は会話の例です。(*実際の会話から要約したものです。)
わい:ロードした jpg 画像ファイルは白黒のはずが全体的に赤くなります。
この不具合の原因を解明し、修正コードを出してください。
Gemini:これはよくある XXX が原因ですね!この ① の方法で治ります!
わい:全然直りませんが、原因は別じゃないの?
Gemini:もしかしたら原因は YYY かもしれません!この ② の方法で!
わい:これも直らないですが
Gemini:失礼しました!やっぱ ① の方法で治るはず!
わい:え?
という繰り返しになりました。
対処法としては自分である程度それっぽい原因のところを新しく指摘すると会話の流れがかわりますが、私はそもそも Rust 初心者なので、わからないものはわからないですね。。。
対象方:別の AI エージェントに聞いてみるか、自分で解決するしか。。。
他の生成 AI も使用してみる
ちなみにこの問題は ChatGPT が解決してくれました。
比べてみるとこんな感じでした。
Gemini は

Gemini は数多なデータから候補を抽出し解答する、という印象が強いです。
なので、その最適な候補がなくなると同じ解答を繰り返すようになった気がします。
(あくまで推測です。事実とは違うかもしれません。)
ちなみに、ChatGPT に聞いてみると違う流れになりました。
(*下記は実際の会話から要約したものです。)
わい:ロードした jpg 画像ファイルは白黒のはずが全体的に赤くなります。
この不具合の原因を解明し、修正コードを出してください。
ChatGPT:(Gemini と同じ原因 ① を指摘してコード生成)
わい:それ直らないですげど
ChatGPT:(Gemini と同じ原因 ② を指摘してコード生成)
わい:それも直らない。。。
ChatGPT:① と ② で直らないですね!やっても直らない「チェックリスト」を作成ました!次は原因 ③ 対応してみよ!
わい:それも直らないですが、
ChatGPT:① と ② と ③ が直らないなら ④ に対応してみましょう!わたしの自信は 95% です!。。。
繰り返し十回ほど。。。
。。。
ChatGPT:① 〜 ⑩までやっても直らないってことは我々が悪いのではなく ”ライブラリ” が悪い可能性が高いですよ!
わい:ライブラリのバーション変更したら治った。。。ありがとう!
ChatGPT:どういたしまして!
ChatGPT は自分の記憶力が短いことを把握し、対策に毎回「チェックリスト」らしきものを作成。以前の発言を忘れないようにしているため、会話がループしていませんでした。
Gemini は、最初のプロンプトに対して非常に正確な解答を素早く出してくれる「瞬発力」が魅力的です。そのため、ユーザー側が的確にディレクションして対話をリードしてあげることで、そのポテンシャルを最大限に引き出せるのだと思います。
一方で ChatGPT は、初期の解答こそ試行錯誤している感がありますが、エラーの要因を一つずつ潰しながら、粘り強く壁打ち相手になってくれるプロセスに強みがあるという印象を受けました。
最後に
最近の生成 AI すごいですね。
Rust を触ったことない私でもこんなに早くアプリを作ることができました。
完璧ではないとはいえ、倍以上の開発スピードになっていると思われます。
ちなみに作成した漫画読みアプリですが、ぜひ使ってみてください!
https://github.com/Lycoris52/RustMangaReader

感想や不具合があれば直しますので、github の issue 作るか、
もしくは huggingface のメッセージ機能で連絡すれば対応いたします。
zenn.dev にもいろいろ公開していますので、
よろしくお願いいたします。
※ 本記事および Zenn 掲載内容は、著者個人の見解・開発活動に基づくものであり、grasys としての公式見解や製品提供を示すものではありません。
※ リンク先の外部サイトは著者個人の運営によるものであり、その内容・動作・安全性について当社は責任を負いかねます。




