///|
/// The parsed `xl/richData/*` and `xl/metadata.xml` parts needed to
/// resolve modern Excel embedded cell images ("Place in cell"). Mirrors
/// the chain Excelize's `getImageCellRel` walks: a cell's `vm` index
/// selects a value-metadata block, whose record points at a rich value,
/// whose structure names a `_rvRel:LocalImageIdentifier` value, which
/// indexes `richValueRel.xml` to a relationship, which resolves to a
/// media part.
struct RichValueImages {
/// value-metadata block index (0-based, i.e. vm-1) -> rich value index
value_metadata : Array[Int]
/// rich values: each is (structure index, ordered values)
rich_values : Array[(Int, Array[String])]
/// rich value structures: each is the ordered list of key names
structures : Array[Array[String]]
/// ordered relationship ids referenced by rich values
rel_ids : Array[String]
/// relationship id -> media target (from richValueRel.xml.rels)
rel_targets : Map[String, String]
/// ordered blip relationship ids of web images, from
/// rdRichValueWebImage.xml (used by the IMAGE() function)
web_blip_ids : Array[String]
/// blip relationship id -> media target (from
/// rdRichValueWebImage.xml.rels)
web_rel_targets : Map[String, String]
/// media target -> manifest-authenticated image identity
media_identities : Map[String, ImagePartIdentity]
}
///|
/// Reads the rich-value image parts from the archive, returning `None`
/// when the workbook has no rich value metadata.
fn parse_rich_value_images(
archive : @zip.Archive,
part_names : Map[String, String],
content_types : @ooxml.PackageContentTypes,
decode : (BytesView) -> String raise XlsxError,
budget? : ReadBudget,
cancelled? : () -> Bool = () => false,
) -> RichValueImages? raise XlsxError {
let metadata_bytes = archive.get("xl/metadata.xml")
let rich_value_bytes = archive.get("xl/richData/rdrichvalue.xml")
guard metadata_bytes is Some(md) && rich_value_bytes is Some(rv) else {
return None
}
let value_metadata = parse_value_metadata(decode(md))
let rich_values = parse_rich_values(decode(rv))
let structures = match archive.get("xl/richData/rdrichvaluestructure.xml") {
Some(value) => parse_rich_value_structures(decode(value))
None => []
}
let rel_ids = match archive.get("xl/richData/richValueRel.xml") {
Some(value) => parse_rich_value_rel_ids(decode(value))
None => []
}
let rich_value_rels_path = actual_archive_part_path(
part_names, "xl/richData/_rels/richValueRel.xml.rels",
)
let raw_rel_targets = match
load_optional_relationship_part(
archive,
content_types,
rich_value_rels_path,
"rich value relationships",
cancelled~,
) {
Some(value) =>
parse_internal_relationship_targets(
decode(value),
rel_image,
budget?,
cancelled~,
)
None => Map([])
}
let rel_targets : Map[String, String] = Map([])
for rel_id, target in raw_rel_targets {
rel_targets[rel_id] = actual_relationship_target_path(
"xl/richData/richValueRel.xml",
target,
part_names,
cancelled~,
)
}
let web_blip_ids = match archive.get("xl/richData/rdRichValueWebImage.xml") {
Some(value) => parse_web_image_blip_ids(decode(value))
None => []
}
let rich_value_web_rels_path = actual_archive_part_path(
part_names, "xl/richData/_rels/rdRichValueWebImage.xml.rels",
)
let raw_web_rel_targets = match
load_optional_relationship_part(
archive,
content_types,
rich_value_web_rels_path,
"rich value web image relationships",
cancelled~,
) {
Some(value) =>
parse_internal_relationship_targets(
decode(value),
rel_image,
budget?,
cancelled~,
)
None => Map([])
}
let web_rel_targets : Map[String, String] = Map([])
for rel_id, target in raw_web_rel_targets {
web_rel_targets[rel_id] = actual_relationship_target_path(
"xl/richData/rdRichValueWebImage.xml",
target,
part_names,
cancelled~,
)
}
Some({
value_metadata,
rich_values,
structures,
rel_ids,
rel_targets,
web_blip_ids,
web_rel_targets,
media_identities: Map([]),
})
}
///|
test "rich value image relationships use source-relative OPC resolution" {
let archive = @zip.Archive::new()
archive.add("xl/metadata.xml", b"")
archive.add("xl/richData/rdrichvalue.xml", b"")
archive.add(
"xl/richData/_rels/richValueRel.xml.rels",
@encoding/utf8.encode(
(
#|
),
),
)
archive.add("decoy.png", b"root image")
archive.add("xl/decoy.png", b"wrong image")
let parsed = parse_rich_value_images(
archive,
archive_part_name_index(archive),
image_reader_test_content_types(),
value => {
@encoding/utf8.decode(value) catch {
_ => raise InvalidXml(msg="invalid utf8")
}
},
)
guard parsed is Some(data) else { fail("expected rich value image data") }
assert_eq(data.rel_targets.get("rId1"), Some("decoy.png"))
}
///|
/// Parses the ordered blip relationship ids from ``
/// elements of `xl/richData/rdRichValueWebImage.xml`. Each web image's
/// `` names the relationship resolving to its media.
fn parse_web_image_blip_ids(xml : String) -> Array[String] raise XlsxError {
let result : Array[String] = []
let mut first = true
for chunk in xml.split("") {
Some(pos) => pos
None => text.length()
}
let srd_body = text[:end]
let blip_attrs = match tag_attributes_in(srd_body, "blip") {
Some(value) => value
None => continue
}
let id = match attr_value(blip_attrs, "r:id") {
Some(value) => Some(value)
None => attr_value(blip_attrs, "id")
}
match id {
Some(value) => result.push(value)
None => continue
}
}
result
}
///|
/// Parses the `` blocks of `xl/metadata.xml`, returning
/// the first record value of each block (the rich value index).
fn parse_value_metadata(xml : String) -> Array[Int] raise XlsxError {
let result : Array[Int] = []
let body = match extract_tag_body_from(xml, "valueMetadata") {
Some(value) => value
None => return result
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => text.length()
}
let bk_body = text[:end]
match tag_attributes_in(bk_body, "rc") {
Some(attrs) =>
match attr_value(attrs, "v") {
Some(v) =>
result.push(@string.parse_int(v, base=10) catch { _ => -1 })
None => result.push(-1)
}
None => result.push(-1)
}
}
result
}
///|
/// Parses `` rich values from `xl/richData/rdrichvalue.xml`: each is
/// its structure index `s` and the ordered `` values.
fn parse_rich_values(
xml : String,
) -> Array[(Int, Array[String])] raise XlsxError {
let result : Array[(Int, Array[String])] = []
// Extract the rvData body first so the split does not also match the
// " value
None => return result
}
let mut first = true
for chunk in body.split("") {
Some(pos) => pos
None => continue
}
let tag = text[:tag_end]
let s = match attr_value(tag, "s") {
Some(value) => @string.parse_int(value, base=10) catch { _ => 0 }
None => 0
}
let end = match text.find("") {
Some(pos) => pos
None => text.length()
}
let rv_body = text[tag_end + 1:end]
let values : Array[String] = []
let mut first_v = true
for v_chunk in rv_body.split("") {
if first_v {
first_v = false
continue
}
let v_text = v_chunk.to_owned()
match v_text.find("") {
Some(pos) => values.push(unescape_xml_text(v_text[:pos]))
None => ()
}
}
result.push((s, values))
}
result
}
///|
/// Parses `` structures from `xl/richData/rdrichvaluestructure.xml`:
/// each is the ordered list of `` key names.
fn parse_rich_value_structures(
xml : String,
) -> Array[Array[String]] raise XlsxError {
let result : Array[Array[String]] = []
let mut first = true
for chunk in xml.split("") {
Some(pos) => pos
None => text.length()
}
let s_body = text[:end]
let keys : Array[String] = []
let mut first_k = true
for k_chunk in s_body.split("
match attr_value(attrs, "n") {
Some(n) => keys.push(unescape_xml_text(n))
None => keys.push("")
}
None => keys.push("")
}
}
result.push(keys)
}
result
}
///|
/// Parses the ordered relationship ids from `` elements of
/// `xl/richData/richValueRel.xml`.
fn parse_rich_value_rel_ids(xml : String) -> Array[String] raise XlsxError {
let result : Array[String] = []
let mut first = true
for chunk in xml.split("") {
Some(pos) => pos
None => continue
}
let tag = text[:end]
let id = match attr_value(tag, "r:id") {
Some(value) => Some(value)
None => attr_value(tag, "id")
}
match id {
Some(value) => result.push(value)
None => ()
}
}
result
}
///|
/// Scans a worksheet's cells for the `vm` (value-metadata) attribute,
/// returning a canonical-reference -> vm-index map. Rich-value cell
/// images are the only cells that carry `vm`, so this is the link
/// between a cell and its rich value.
fn parse_cell_vm_map(sheet_xml : String) -> Map[String, Int] raise XlsxError {
let result : Map[String, Int] = Map([])
let mut first = true
for chunk in sheet_xml.split("") {
Some(pos) => pos
None => continue
}
let tag = text[:end]
let vm = match attr_value(tag, "vm") {
Some(value) => value
None => continue
}
let reference = match attr_value(tag, "r") {
Some(value) => value
None => continue
}
let vm_index = @string.parse_int(vm, base=10) catch { _ => continue }
let (row, col) = cell_ref_to_rc(reference)
result[cell_ref_from(row, col)] = vm_index
}
result
}
///|
/// Resolves a rich value's local ("Place in cell") image media target
/// via the `_rvRel:LocalImageIdentifier` key, or `None` when the value
/// is not a local image.
fn rich_value_local_target(
data : RichValueImages,
keys : Array[String],
values : Array[String],
) -> String? {
let local_idx = rich_value_key_index(keys, "_rvRel:LocalImageIdentifier")
if local_idx < 0 || local_idx >= values.length() {
return None
}
let rel_index = @string.parse_int(values[local_idx], base=10) catch {
_ => return None
}
if rel_index < 0 || rel_index >= data.rel_ids.length() {
return None
}
data.rel_targets.get(data.rel_ids[rel_index])
}
///|
/// Resolves a rich value's web (IMAGE function) image media target via
/// the `WebImageIdentifier` key, or `None` when the value is not a web
/// image. Mirrors Excelize's getRichDataWebImagesRel.
fn rich_value_web_target(
data : RichValueImages,
keys : Array[String],
values : Array[String],
) -> String? {
let web_idx = rich_value_key_index(keys, "WebImageIdentifier")
if web_idx < 0 || web_idx >= values.length() {
return None
}
let web_index = @string.parse_int(values[web_idx], base=10) catch {
_ => return None
}
if web_index < 0 || web_index >= data.web_blip_ids.length() {
return None
}
data.web_rel_targets.get(data.web_blip_ids[web_index])
}
///|
/// Returns the index of a key name within a rich value structure, or -1.
fn rich_value_key_index(keys : Array[String], name : String) -> Int {
for i, key in keys {
if key == name {
return i
}
}
-1
}
///|
/// Resolves the modern "Place in cell" embedded image shown in
/// `reference`, mirroring Excelize's `getImageCellRel`
/// (`_rvRel:LocalImageIdentifier` path). Returns `None` when the cell has
/// no rich-value image.
fn Workbook::rich_value_image_for_cell(
self : Workbook,
sheet : Worksheet,
reference : StringView,
) -> Image? raise XlsxError {
let data = match self.rich_value_images {
Some(value) => value
None => return None
}
let canonical = {
let (row, col) = cell_ref_to_rc(reference)
cell_ref_from(row, col)
}
let vm = match sheet.cell_vm.get(canonical) {
Some(value) => value
None => return None
}
// Only #VALUE! cells carry an embedded rich-value image.
if sheet.get_cell(canonical) != Some(formula_error_value) {
return None
}
if vm < 1 || vm > data.value_metadata.length() {
return None
}
let rich_value_idx = data.value_metadata[vm - 1]
if rich_value_idx < 0 || rich_value_idx >= data.rich_values.length() {
return None
}
let (structure_idx, values) = data.rich_values[rich_value_idx]
if structure_idx < 0 || structure_idx >= data.structures.length() {
return None
}
let keys = data.structures[structure_idx]
// Excelize requires the structure keys and rich value values to line
// up before reading any of them (getImageCellRel:
// len(rvStruct.K) != len(rv.V)); a mismatched rich value is not an
// image.
if keys.length() != values.length() {
return None
}
let alt_text = {
let text_idx = rich_value_key_index(keys, "Text")
if text_idx >= 0 && text_idx < values.length() {
values[text_idx]
} else {
""
}
}
// A rich value carries either a local image (Place in cell) or a web
// image (IMAGE function). Excelize's getImageCellRel branches on which
// key is *present*: once a LocalImageIdentifier key exists it commits
// to the local path and never falls back to the web path, even if the
// local relationship fails to resolve.
let target = if rich_value_key_index(keys, "_rvRel:LocalImageIdentifier") !=
-1 {
rich_value_local_target(data, keys, values)
} else {
rich_value_web_target(data, keys, values)
}
let media_path = match target {
Some(value) => value
None => return None
}
match self.rich_value_media.get(media_path) {
Some(bytes) => {
let identity = match data.media_identities.get(media_path) {
Some(value) => value
None => return None
}
Some({
reference: canonical,
data: bytes,
extension: "." + identity.extension,
content_type: identity.content_type,
offset_x: 0,
offset_y: 0,
scale_x: 1.0,
scale_y: 1.0,
width_emu: 0,
height_emu: 0,
print_object: true,
locked: false,
hyperlink: "",
hyperlink_type: Unset,
name: "",
alt_text,
lock_aspect_ratio: false,
positioning: OneCell,
drawing_offset_x_emu: None,
drawing_offset_y_emu: None,
drawing_width_emu: None,
drawing_height_emu: None,
drawing_order: None,
})
}
None => None
}
}
///|
test "parse_rich_values does not create a phantom entry from rvData" {
let xml =
#|
#|
#| 0alt one
#| 2
#|
let values = parse_rich_values(xml)
// exactly two rich values, index 0 is the first (not a phantom)
inspect(values.length(), content="2")
let (s0, v0) = values[0]
inspect(s0, content="0")
debug_inspect(
v0,
content=(
#|["0", "alt one"]
),
)
let (s1, v1) = values[1]
inspect(s1, content="1")
debug_inspect(
v1,
content=(
#|["2"]
),
)
}
///|
test "parse_value_metadata reads block record values" {
let xml =
#|
#|
#|
#|
#|
#|
debug_inspect(
parse_value_metadata(xml),
content=(
#|[0, 3]
),
)
}
///|
test "parse_rich_value_structures extracts ordered key names" {
let xml =
#|
#|
#|
#|
#|
#|
let structures = parse_rich_value_structures(xml)
inspect(structures.length(), content="1")
debug_inspect(
structures[0],
content=(
#|["_rvRel:LocalImageIdentifier", "Text"]
),
)
}
///|
test "parse_rich_value_rel_ids reads ordered relationship ids" {
let xml =
#|
#|
#|
#|
debug_inspect(
parse_rich_value_rel_ids(xml),
content=(
#|["rId1", "rId2"]
),
)
}
///|
test "parse_web_image_blip_ids reads ordered blip relationship ids" {
let xml =
#|
#|
#|
#|
debug_inspect(
parse_web_image_blip_ids(xml),
content=(
#|["rId5", "rId6"]
),
)
}