bookmeta: with no cover named, take only the record at first_image

When a MOBI or AZW3 file names no cover (no EXTH 201 or 202), the cover is
the record at the first image index if it is a JPEG, PNG or GIF, and
nothing otherwise, as calibre does (metadata/mobi.py:524-532). The walk to
the next image is gone: a later record may be an inline illustration, and
kindlegen puts a font or RESC record first.

EXTH 121 is no longer read, which also stops a stale KF8 boundary below
first_image from hiding the cover.
This commit is contained in:
funkpower committed 2026-11-13 15:22:04 -05:00
1 parent 130324bbe0
commit a105b1c9aa
1 file changed
+46 -26
+46 -26
View File
@@ -65,7 +65,6 @@ const PUBLISHED: u32 = 106;
const ASIN: u32 = 113;
const COVER_OFFSET: u32 = 201;
const THUMB_OFFSET: u32 = 202;
const KF8_BOUNDARY: u32 = 121;
const UPDATED_TITLE: u32 = 503;
const LANGUAGE: u32 = 524;
@@ -100,7 +99,6 @@ struct Exth {
asin: Option<String>,
cover: Option<u32>,
thumb: Option<u32>,
kf8_boundary: Option<u32>,
title: Option<String>,
language: Option<String>,
}
@@ -196,7 +194,7 @@ fn fill(meta: &mut Metadata, bytes: &[u8]) -> Result<(), String> {
if let Some(offset) = exth.cover.or(exth.thumb) {
meta.cover = cover(bytes, &records, first_image, offset, meta);
} else {
meta.cover = first_image_record(bytes, &records, first_image, exth.kf8_boundary);
meta.cover = first_image_record(bytes, &records, first_image);
}
Ok(())
}
@@ -291,7 +289,6 @@ fn exth(
LANGUAGE => set(&mut out.language, decode(data)),
COVER_OFFSET => set(&mut out.cover, number(data)),
THUMB_OFFSET => set(&mut out.thumb, number(data)),
KF8_BOUNDARY => set(&mut out.kf8_boundary, number(data)),
_ => {}
}
}
@@ -341,26 +338,19 @@ fn cover(
})
}
/// The cover when the file names none: calibre takes the first image record.
/// The cover when the file names none: calibre takes the record at the first
/// image index, and only that one.
///
/// The search runs from the first image record to the end of the MOBI 6
/// records, the KF8 boundary when the file is a joint one, and skips records
/// that are not an image.
fn first_image_record(
bytes: &[u8],
records: &[usize],
first_image: Option<u32>,
boundary: Option<u32>,
) -> Option<Cover> {
/// When that record is not a JPEG, PNG or GIF there is no cover. The search
/// does not walk on to the next image, because a later record may be an inline
/// illustration, and kindlegen puts a font or a RESC record first.
fn first_image_record(bytes: &[u8], records: &[usize], first_image: Option<u32>) -> Option<Cover> {
let first = first_image.filter(|&index| index != NONE)?;
let stop = boundary.map_or(records.len(), |at| (at as usize).min(records.len()));
(first as usize..stop).find_map(|index| {
let data = record(bytes, records, index)?;
let media_type = image_type(data).filter(|kind| *kind != "image/bmp")?;
Some(Cover {
media_type: media_type.to_string(),
bytes: data.to_vec(),
})
let data = record(bytes, records, first as usize)?;
let media_type = image_type(data).filter(|kind| *kind != "image/bmp")?;
Some(Cover {
media_type: media_type.to_string(),
bytes: data.to_vec(),
})
}
@@ -666,7 +656,17 @@ mod tests {
}
#[test]
fn with_no_cover_named_the_first_real_image_record_is_the_cover() {
fn with_no_cover_named_only_the_record_at_first_image_can_be_the_cover() {
let book = Book {
images: vec![JPEG.to_vec(), b"GIF89a-later".to_vec()],
..Book::default()
};
let cover = read("x.mobi", &book.bytes()).cover.expect("a cover");
assert_eq!(cover.media_type, "image/jpeg");
assert_eq!(cover.bytes, JPEG);
// The record at first_image is not an image: no cover, even though a
// later record is one.
let book = Book {
images: vec![
b"not an image".to_vec(),
@@ -675,9 +675,7 @@ mod tests {
],
..Book::default()
};
let cover = read("x.mobi", &book.bytes()).cover.expect("a cover");
assert_eq!(cover.media_type, "image/jpeg");
assert_eq!(cover.bytes, JPEG);
assert_eq!(read("x.mobi", &book.bytes()).cover, None);
let book = Book {
images: vec![b"not an image".to_vec()],
@@ -686,6 +684,28 @@ mod tests {
assert_eq!(read("x.mobi", &book.bytes()).cover, None);
}
#[test]
fn a_font_at_first_image_is_not_skipped_past_to_an_inline_image() {
// No EXTH 201 or 202. The first resource record is a font, as in
// kindlegen output; calibre reads only that record and finds no cover.
let book = Book {
images: vec![b"FONT\0\0\0\x18".to_vec(), JPEG.to_vec()],
..Book::default()
};
assert_eq!(read("x.mobi", &book.bytes()).cover, None);
}
#[test]
fn a_stale_kf8_boundary_below_first_image_does_not_hide_the_cover() {
let book = Book {
exth: exth(&[(121, &1u32.to_be_bytes())]),
first_image: 2,
images: vec![b"text".to_vec(), JPEG.to_vec()],
..Book::default()
};
assert!(read("x.mobi", &book.bytes()).cover.is_some());
}
#[test]
fn the_first_image_search_stops_at_the_kf8_boundary() {
let book = Book {