///|
// Block images (PNG and JPEG), after asciidoctor-pdf's `convert_image`:
// the width comes from `pdfwidth`, `scaledwidth` or `width`, else the
// intrinsic width at 0.75 pt/px capped to the bounds; an image that does not
// fit below the cursor moves to the next page, or shrinks to fit a page; the
// title is a caption below.
///|
fn be32(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 24) |
(data[offset + 1].to_int() << 16) |
(data[offset + 2].to_int() << 8) |
data[offset + 3].to_int()
}
///|
/// Intrinsic (width, height) in pixels and the media type, for PNG and
/// JPEG.
fn image_info(data : Bytes) -> (Int, Int, String)? {
if data.length() > 24 &&
data[0] == b'\x89' &&
data[1] == b'P' &&
data[2] == b'N' &&
data[3] == b'G' {
return Some((be32(data, 16), be32(data, 20), "image/png"))
}
if data.length() > 4 && data[0] == b'\xFF' && data[1] == b'\xD8' {
let mut i = 2
while i + 9 < data.length() {
if data[i] != b'\xFF' {
i += 1
continue
}
let marker = data[i + 1].to_int()
let length = read_u16(data, i + 2)
// start-of-frame markers carry the dimensions
if marker >= 0xC0 &&
marker <= 0xCF &&
marker != 0xC4 &&
marker != 0xC8 &&
marker != 0xCC {
let height = read_u16(data, i + 5)
let width = read_u16(data, i + 7)
return Some((width, height, "image/jpeg"))
}
i += 2 + length
}
}
None
}
///|
/// A length attribute in points: `%` of `bounds`, `in`, `mm`, `cm`, `pt`,
/// `px` or a bare number of pixels.
fn resolve_length(
value : String,
bounds : Double,
bare_px? : Bool = true,
) -> Double? {
let number = fn(text : String) -> Double? {
Some(@string.parse_double(text)) catch {
_ => None
}
}
if value.has_suffix("%") {
number(value[:value.length() - 1].to_owned()).map(v => v / 100.0 * bounds)
} else if value.has_suffix("in") {
number(value[:value.length() - 2].to_owned()).map(v => v * 72.0)
} else if value.has_suffix("mm") {
number(value[:value.length() - 2].to_owned()).map(v => v * 72.0 / 25.4)
} else if value.has_suffix("cm") {
number(value[:value.length() - 2].to_owned()).map(v => v * 72.0 / 2.54)
} else if value.has_suffix("pt") {
number(value[:value.length() - 2].to_owned())
} else if value.has_suffix("px") {
number(value[:value.length() - 2].to_owned()).map(v => v * 0.75)
} else if bare_px {
number(value).map(v => v * 0.75)
} else {
number(value)
}
}
///|
fn Converter::convert_image(
self : Converter,
node : @core.Node,
relative_to_imagesdir? : Bool = true,
) -> Unit {
let flow = self.flow
let target = node.attr("target").unwrap_or("")
let (image_target, format) = image_target_and_format(
target,
node.attr("format"),
)
let (data, path) = if format == "gif" {
(None, "")
} else {
load_image_target(
node,
image_target,
start?=if relative_to_imagesdir {
Some(node.document().attr("imagesdir").unwrap_or(""))
} else {
None
},
)
}
let path = if image_target is Data(_) { target } else { path }
let info = data.bind(image_info)
// `float` (left or right), else `align` (left when unknown), else the
// last alignment role
let alignment = match node.attr("float") {
Some("left") => "left"
Some("right") => "right"
_ =>
match node.attr("align") {
Some("center") => "center"
Some("right") => "right"
Some(_) => "left"
None =>
node
.roles()
.rev_iter()
.find_first(r => r == "left" || r == "center" || r == "right")
.unwrap_or("left")
}
}
let svg = format == "svg"
if data is Some(bytes) && svg {
if self.convert_svg_image(node, path, bytes, alignment) {
return
}
}
guard !svg && data is Some(bytes) && info is Some((px_w, px_h, mime)) else {
if data is Some(_) && !svg {
// asciidoctor-pdf draws SVG, GIF, ... itself
unsupported("image", "format of \{target}", node)
}
// asciidoctor-pdf's `on_image_error`: the alt text in brackets (linked
// when the image is) and the target in italics, then the caption
let (open, close) = match node.attr("link") {
Some(link) => ("", "")
None => ("", "")
}
self.ink_prose(
"\{open}[\{node.attr("alt").unwrap_or(target)}]\{close} | \{target}",
align=match alignment {
"center" => Center
"right" => Right
_ => Left
},
margin_bottom=0.0,
normalize=false,
)
if node.has_title() {
self.ink_caption(node.captioned_title(), bottom=true)
}
self.block_margin_bottom(node)
return
}
let bounds = flow.width()
let intrinsic = px_w.to_double() * 0.75
// resolve_explicit_width: `pdfwidth` (`iw`: of the intrinsic width),
// `scale`, `scaledwidth`, then `width` (a percentage, or digits taken
// as pixels, and no wider than the bounds; other values are ignored)
let explicit = match node.attr("pdfwidth") {
Some(w) if w.has_suffix("iw") => {
let percent = @string.parse_double(w[:w.length() - 2].to_owned()) catch {
_ => 100.0
}
Some(percent / 100.0 * intrinsic)
}
Some(w) => resolve_length(w, bounds, bare_px=false)
None =>
match node.attr("scale") {
Some(s) =>
Some(
(@string.parse_double(s) catch { _ => 100.0 }) / 100.0 * intrinsic,
)
None =>
match node.attr("scaledwidth") {
Some(w) => resolve_length(w, bounds, bare_px=false)
None =>
match node.attr("width") {
Some(w) if w.has_suffix("%") =>
resolve_length(w, bounds).map(v => @cmp.minimum(v, bounds))
Some(w) if w != "" && w.iter().all(c => c >= '0' && c <= '9') =>
resolve_length(w, bounds).map(v => @cmp.minimum(v, bounds))
_ => None
}
}
}
}
let mut width = match explicit {
Some(w) => w
None => @cmp.minimum(intrinsic, bounds)
}
let aspect = px_h.to_double() / px_w.to_double()
let mut height = width * aspect
// the image keeps its caption (below it) on the same page
let caption_height = self.image_caption_height(node, alignment, explicit)
if height > flow.cursor() - caption_height {
if !self.pinned && !flow.at_page_top() && !first_child_at_top(node) {
flow.advance_page()
}
if height > flow.cursor() - caption_height {
height = flow.cursor() - caption_height
width = height / aspect
}
}
self.add_dest(node)
self.ink_image_caption(node, alignment, width, top=true)
let x = match alignment {
"center" => flow.left + (bounds - width) / 2.0
"right" => flow.left + bounds - width
_ => flow.left
}
flow.push(
Image({
x_pt: x,
y_pt: flow.y,
w_pt: width,
h_pt: height,
data: bytes,
mime,
}),
)
// add_link_to_image: a link annotation over the image
match node.attr("link") {
Some(link) if link != "" =>
flow.push(
Link({
x_pt: x,
y_pt: flow.y,
w_pt: width,
h_pt: height,
// add_link_to_image: `#name` is a destination, anything else a
// URI action
target: if link.has_prefix("#") {
Named(pdf_anchor_name(link[1:].to_owned()))
} else {
Uri(link)
},
}),
)
_ => ()
}
flow.move_down(height)
self.finish_image(node, alignment, width, height, caption_height)
}
///|
/// An inline image (`image:target[]`): its bytes and media type, its
/// intrinsic size in pixels, the width asciidoctor-pdf gives its `
`
/// (points, `N%` of the line, or `N%W`: N% of the intrinsic width W) and
/// its `fit`.
priv struct InlineImage {
data : Bytes
mime : String
px_w : Int
px_h : Int
width : String
fit : String?
/// an SVG image: the document (drawn at the size its line gives it), its
/// path and the image node
svg : (@svg.SvgDocument, String, @core.Node)?
}
///|
/// asciidoctor-pdf's `convert_inline_image`: a readable PNG or JPEG becomes
/// an `
` whose `src` numbers it among the session's inline images (the
/// formatted text then reserves its width in the line and draws it there,
/// see `inline_image_size`); any other image is its alt text in brackets.
/// A `link` makes it a link.
fn convert_inline_image(node : @core.Node) -> String {
let target = node.target.unwrap_or("")
let alt = node.attr("alt").unwrap_or("")
let (image_target, format) = image_target_and_format(
target,
node.attr("format"),
)
let (bytes, path) = if format == "gif" {
(None, "")
} else {
load_image_target(
node,
image_target,
start=node.document().attr("imagesdir").unwrap_or(""),
)
}
let path = if image_target is Data(_) { target } else { path }
let svg = format == "svg"
let loaded = bytes.bind(data => {
if svg {
// prawn-svg's intrinsic size, in the bounds of the page's content
inline_svg(data, path, node).map(doc => {
let s = doc.sizing
(data, (1, 1, "image/svg+xml"), s.output_width, Some((doc, path, node)))
})
} else {
image_info(data).map(info => (data, info, info.0.to_double() * 0.75, None))
}
})
let img = match loaded {
Some((data, (px_w, px_h, mime), intrinsic, svg_image)) => {
// resolve_explicit_width without bounds: a percentage stays one (of
// the intrinsic width, in a table cell), `iw` and `scale` are of the
// intrinsic width, digits are pixels; else the intrinsic width
let percent = fn(value : String) {
@string.parse_double(value) catch {
_ => 100.0
}
}
let width = match node.attr("pdfwidth") {
Some(w) if w.has_suffix("%") => w
Some(w) if w.has_suffix("iw") =>
(percent(w[:w.length() - 2].to_owned()) / 100.0 * intrinsic).to_string()
Some(w) =>
resolve_length(w, 0.0, bare_px=false)
.map(v => v.to_string())
.unwrap_or(intrinsic.to_string())
None =>
match node.attr("scale") {
Some(s) => (percent(s) / 100.0 * intrinsic).to_string()
None =>
match node.attr("scaledwidth") {
Some(w) if w.has_suffix("%") => w
Some(w) =>
resolve_length(w, 0.0, bare_px=false)
.map(v => v.to_string())
.unwrap_or(intrinsic.to_string())
None =>
match node.attr("width") {
Some(w) if w.has_suffix("%") => w
Some(w) if w != "" &&
w.iter().all(c => c >= '0' && c <= '9') =>
(percent(w) * 0.75).to_string()
_ => intrinsic.to_string()
}
}
}
}
let in_cell = match node.parent() {
Some(parent) => parent.context == TableCell
None => false
}
let width = if in_cell && width.has_suffix("%") {
width + intrinsic.to_string()
} else {
width
}
let index = session().inline_images.length()
session().inline_images.push({
data,
mime,
px_w,
px_h,
width,
fit: node.attr("fit"),
svg: svg_image,
})
"
"
}
None => {
// a missing image is its alt text in Ruby too; asciidoctor-pdf draws
// GIF (with prawn-gmagick) images this backend cannot
if bytes is Some(_) && !svg {
unsupported("inline", "image \{target}", node)
}
"[\{alt}]"
}
}
match node.attr("link") {
// Ruby's link annotation over an inline image names only the first
// character of the anchor as its destination (`/Dest (_)` for
// `link=#_section`), which this reproduces
Some(link) if link.has_prefix("#") => {
let anchor = if img.has_prefix("
\{img}"
}
Some(link) if link != "" => "\{img}"
_ => img
}
}
///|
/// The size an inline image takes in a line `available` wide
/// (InlineImageArranger#arrange_images): its width (a percentage of the
/// line, or of its intrinsic width, or points, never wider than the line)
/// and height, shrunk to fit `max_height` (the bounds, or the line for
/// `fit=line`); and whether it is taller than one and a half lines of
/// `font_height`, when the line grows to hold it.
fn inline_image_size(
image : InlineImage,
available : Double,
font_height : Double,
) -> (Double, Double, Bool) {
let number = fn(text : String) {
@string.parse_double(text) catch {
_ => 0.0
}
}
let spec = image.width
let mut width = match spec.find("%") {
Some(i) if i + 1 < spec.length() =>
@cmp.minimum(
available,
number(spec[:i].to_owned()) / 100.0 * number(spec[i + 1:].to_owned()),
)
Some(i) =>
@cmp.minimum(available, number(spec[:i].to_owned()) / 100.0 * available)
None => @cmp.minimum(available, number(spec))
}
let max_height = if image.fit == Some("line") {
@cmp.minimum(content_height(), font_height)
} else {
content_height()
}
match image.svg {
Some((doc, _, _)) => {
// prawn-svg sized to the width, then to the height if too tall
let sizing = doc.sizing
sizing.resize(width~)
let mut height = sizing.output_height
if height > max_height {
sizing.resize(height=max_height)
height = max_height
}
width = sizing.output_width
return (width, height, height > font_height * 1.5)
}
None => ()
}
let aspect = image.px_h.to_double() / image.px_w.to_double()
let mut height = width * aspect
if height > max_height {
height = max_height
width = height / aspect
}
(width, height, height > font_height * 1.5)
}
///|
/// The height of a page's content area: the bounds an inline image must
/// fit in.
fn content_height() -> Double {
let setup = page_setup.val
setup.height - setup.margin_top - setup.margin_bottom
}