///|
/// Synchronous intrinsic-size lookup for images rendered by markdown.
///
/// The rich-text layout must reserve an image's final box before the
/// renderer ever decodes it, so percentage-width galleries and plain inline
/// images need the intrinsic aspect ratio at projection time. The host
/// registers a byte reader (the native backends read image files from the
/// filesystem); this module only inspects image headers, never pixel data,
/// and stays pure so it compiles for every target. When no reader is
/// registered, or a source cannot be read (remote URLs, missing files,
/// unknown formats), the lookup yields `None` and callers fall back to the
/// placeholder box.
let markdown_image_metrics_reader : Ref[(String) -> Bytes?] = Ref(_ => None)
///|
/// Header sizes keyed by source. `None` values are cached too, so a missing
/// or unreadable file is probed once instead of on every layout pass.
let markdown_image_metrics_sizes : Map[String, @core.Size?] = Map([])
///|
/// Register the host reader that returns the leading bytes of a local image
/// source. Passing a `None`-returning reader restores pure-fallback mode.
pub fn markdown_set_image_metrics_reader(reader : (String) -> Bytes?) -> Unit {
markdown_image_metrics_reader.val = reader
markdown_image_metrics_sizes.clear()
}
///|
/// Host reader for remote (`http:`/`https:`) image sources, consulted by the
/// intrinsic-size probe. Remote bytes are ordinarily fetched asynchronously,
/// so this reader is optional: without one, a remote source keeps the
/// placeholder box until its real size arrives with the load completion.
let markdown_remote_image_reader : Ref[(String) -> Bytes?] = Ref(_ => None)
///|
/// Register the reader that resolves remote image sources for the size probe.
/// Passing a `None`-returning reader restores pure-fallback mode.
pub fn markdown_set_remote_image_reader(reader : (String) -> Bytes?) -> Unit {
markdown_remote_image_reader.val = reader
markdown_image_metrics_sizes.clear()
}
///|
/// Feed asynchronously fetched image bytes into the intrinsic-size cache.
///
/// Remote sources are fetched outside the layout pass, so their real size only
/// becomes available at load completion; noting it lets later projections
/// reserve the correct box instead of the placeholder ratio. Returns `true`
/// when the cached size for `source` changed, which is the signal to re-project
/// the document. Unusable bytes never overwrite a cached size.
pub fn markdown_image_metrics_note_bytes(
source : String,
bytes : Bytes,
) -> Bool {
if source == "" {
return false
}
let size = markdown_image_metrics_size_from_bytes(bytes)
match size {
None => false
Some(new_size) => {
let changed = match markdown_image_metrics_sizes.get(source) {
Some(Some(previous)) =>
previous.width != new_size.width || previous.height != new_size.height
_ => true
}
if markdown_image_metrics_sizes.length() >= 1024 {
markdown_image_metrics_sizes.clear()
}
markdown_image_metrics_sizes.set(source, size)
if changed {
// The projected rich text for image-bearing blocks depends on these
// sizes; drop the cached conversions so the next projection reserves
// the corrected box instead of the placeholder ratio.
rich_text_block_conversions.clear()
}
changed
}
}
}
///|
/// Return the intrinsic pixel size of a resolved image source, or `None`
/// when the format is unknown, the source is remote without a registered
/// reader, or it cannot be read.
pub fn markdown_image_metrics_size(source : String) -> @core.Size? {
if source == "" || source.has_prefix("data:") || source.has_prefix("file://") {
return None
}
match markdown_image_metrics_sizes.get(source) {
Some(cached) => cached
None => {
let size = if source.has_prefix("http://") ||
source.has_prefix("https://") {
match (markdown_remote_image_reader.val)(source) {
Some(bytes) => markdown_image_metrics_size_from_bytes(bytes)
None => None
}
} else {
match (markdown_image_metrics_reader.val)(source) {
Some(bytes) => markdown_image_metrics_size_from_bytes(bytes)
None => None
}
}
if markdown_image_metrics_sizes.length() >= 1024 {
markdown_image_metrics_sizes.clear()
}
markdown_image_metrics_sizes.set(source, size)
size
}
}
}
///|
/// Parse the intrinsic size from the leading bytes of a PNG, JPEG, GIF,
/// BMP, WebP or SVG image.
pub fn markdown_image_metrics_size_from_bytes(bytes : Bytes) -> @core.Size? {
// SVG has no raster header; the shared vector probe reads its viewBox or
// root width/height instead. An unrecognized payload returns `None` and
// falls through to the raster header scan below.
match @render_common.svg_intrinsic_size_from_bytes(bytes) {
Some(size) => return Some(size)
None => ()
}
let length = bytes.length()
let size = if length >= 24 &&
markdown_image_metrics_bytes_are(bytes, 0, [0x89, 0x50, 0x4e, 0x47]) &&
markdown_image_metrics_bytes_are(bytes, 12, [0x49, 0x48, 0x44, 0x52]) {
Some(
@core.Size::new(
width=markdown_image_metrics_be32(bytes, 16).to_double(),
height=markdown_image_metrics_be32(bytes, 20).to_double(),
),
)
} else if length >= 10 &&
markdown_image_metrics_bytes_are(bytes, 0, [0x47, 0x49, 0x46]) {
Some(
@core.Size::new(
width=markdown_image_metrics_le16(bytes, 6).to_double(),
height=markdown_image_metrics_le16(bytes, 8).to_double(),
),
)
} else if length >= 26 &&
markdown_image_metrics_bytes_are(bytes, 0, [0x42, 0x4d]) {
let height = markdown_image_metrics_le32(bytes, 22)
Some(
@core.Size::new(
width=markdown_image_metrics_le32(bytes, 18).to_double(),
height=(if height < 0 { -height } else { height }).to_double(),
),
)
} else if length >= 25 &&
markdown_image_metrics_bytes_are(bytes, 0, [0x52, 0x49, 0x46, 0x46]) &&
markdown_image_metrics_bytes_are(bytes, 8, [0x57, 0x45, 0x42, 0x50]) {
markdown_image_metrics_webp_size(bytes, length)
} else if length >= 12 &&
markdown_image_metrics_bytes_are(bytes, 0, [0xff, 0xd8]) {
markdown_image_metrics_jpeg_size(bytes, length)
} else {
None
}
match size {
Some(size) if size.width > 0.0 && size.height > 0.0 => Some(size)
_ => None
}
}
///|
fn markdown_image_metrics_webp_size(bytes : Bytes, length : Int) -> @core.Size? {
if markdown_image_metrics_byte(bytes, 12) != 0x56 ||
markdown_image_metrics_byte(bytes, 13) != 0x50 ||
markdown_image_metrics_byte(bytes, 14) != 0x38 {
return None
}
match markdown_image_metrics_byte(bytes, 15) {
// "VP8 ": lossy; the dimensions sit behind the start-code at 23..25.
0x20 =>
if length >= 30 {
Some(
@core.Size::new(
width=(markdown_image_metrics_le16(bytes, 26) & 0x3fff).to_double(),
height=(markdown_image_metrics_le16(bytes, 28) & 0x3fff).to_double(),
),
)
} else {
None
}
// "VP8L": the 14-bit width-1/height-1 pair follows the 0x2f signature.
0x4c =>
if length >= 25 {
let bits = markdown_image_metrics_le32(bytes, 21)
Some(
@core.Size::new(
width=((bits & 0x3fff) + 1).to_double(),
height=(((bits >> 14) & 0x3fff) + 1).to_double(),
),
)
} else {
None
}
// "VP8X": 24-bit canvas size minus one at 24 and 27.
0x58 =>
if length >= 30 {
Some(
@core.Size::new(
width=(markdown_image_metrics_le24(bytes, 24) + 1).to_double(),
height=(markdown_image_metrics_le24(bytes, 27) + 1).to_double(),
),
)
} else {
None
}
_ => None
}
}
///|
/// Scan JPEG marker segments for a start-of-frame carrying the dimensions.
fn markdown_image_metrics_jpeg_size(bytes : Bytes, length : Int) -> @core.Size? {
let mut index = 2
while index + 9 <= length {
if markdown_image_metrics_byte(bytes, index) != 0xff {
index = index + 1
continue
}
let marker = markdown_image_metrics_byte(bytes, index + 1)
if marker == 0xff {
index = index + 1
continue
}
// Standalone markers (RST, SOI, EOI, TEM) carry no length field.
if (marker >= 0xd0 && marker <= 0xd9) || marker == 0x01 {
index = index + 2
continue
}
if (marker >= 0xc0 && marker <= 0xc3) ||
(marker >= 0xc5 && marker <= 0xc7) ||
(marker >= 0xc9 && marker <= 0xcb) ||
(marker >= 0xcd && marker <= 0xcf) {
return Some(
@core.Size::new(
width=markdown_image_metrics_be16(bytes, index + 7).to_double(),
height=markdown_image_metrics_be16(bytes, index + 5).to_double(),
),
)
}
index = index + 2 + markdown_image_metrics_be16(bytes, index + 2)
}
None
}
///|
fn markdown_image_metrics_byte(bytes : Bytes, index : Int) -> Int {
match bytes.get(index) {
Some(byte) => byte.to_int()
None => -1
}
}
///|
fn markdown_image_metrics_bytes_are(
bytes : Bytes,
start : Int,
expected : Array[Int],
) -> Bool {
for offset, want in expected {
if markdown_image_metrics_byte(bytes, start + offset) != want {
return false
}
}
true
}
///|
fn markdown_image_metrics_be16(bytes : Bytes, start : Int) -> Int {
(markdown_image_metrics_byte(bytes, start) << 8) |
markdown_image_metrics_byte(bytes, start + 1)
}
///|
fn markdown_image_metrics_be32(bytes : Bytes, start : Int) -> Int {
(markdown_image_metrics_be16(bytes, start) << 16) |
markdown_image_metrics_be16(bytes, start + 2)
}
///|
fn markdown_image_metrics_le16(bytes : Bytes, start : Int) -> Int {
markdown_image_metrics_byte(bytes, start) |
(markdown_image_metrics_byte(bytes, start + 1) << 8)
}
///|
fn markdown_image_metrics_le24(bytes : Bytes, start : Int) -> Int {
markdown_image_metrics_le16(bytes, start) |
(markdown_image_metrics_byte(bytes, start + 2) << 16)
}
///|
fn markdown_image_metrics_le32(bytes : Bytes, start : Int) -> Int {
markdown_image_metrics_le16(bytes, start) |
(markdown_image_metrics_le16(bytes, start + 2) << 16)
}