///|
/// A Kingsoft WPS Office embedded cell image, extracted from
/// `xl/cellimages.xml` and its relationships. The `name` is the image
/// identifier a `DISPIMG` formula references; `data` is the raw media,
/// `extension` its file extension (with the leading dot, e.g. ".png"),
/// and `alt_text` the descriptive text from `cNvPr@descr`.
struct CellImage {
name : String
data : Bytes
extension : String
content_type : String
alt_text : String
}
///|
/// Parses `xl/cellimages.xml` plus its relationships into the list of
/// embedded cell images, loading each referenced media part from the
/// archive. Mirrors Excelize's `cellImagesReader` / `getDispImages`.
/// Images whose media part is absent are skipped, like Go.
fn parse_cell_images(
cell_images_xml : StringView,
rels_xml : StringView,
source_part : StringView,
part_names : Map[String, String],
content_types : @ooxml.PackageContentTypes,
archive : @zip.Archive,
budget? : ReadBudget,
cancelled? : () -> Bool = () => false,
) -> Array[CellImage] raise XlsxError {
let images : Array[CellImage] = []
let targets = if rels_xml is "" {
Map([])
} else {
// cellimages relationships are always image relationships
parse_internal_relationship_targets(
rels_xml,
rel_image,
budget?,
cancelled~,
)
}
let xml_str = cell_images_xml.to_owned()
// Each embedded image is one <...pic> element; cellimages.xml contains
// only cell images, so every pic here is one. The WPS namespace prefix
// (xdr:) is matched, falling back to an unprefixed .
let (open_tag, close_tag) = if xml_str.contains("")
} else {
("")
}
let mut first = true
for chunk in xml_str.split(open_tag) {
if first {
first = false
continue
}
let text = chunk.to_owned()
let end = match text.find(close_tag) {
Some(pos) => pos
None => continue
}
let pic_body = text[:end]
let cnv_attrs = match tag_attributes_in(pic_body, "xdr:cNvPr") {
Some(value) => value
None =>
match tag_attributes_in(pic_body, "cNvPr") {
Some(value) => value
None => continue
}
}
let name = match attr_value(cnv_attrs, "name") {
Some(value) => value
None => continue
}
let alt_text = match attr_value(cnv_attrs, "descr") {
Some(value) => value
None => ""
}
// Narrow to the blipFill body first: an unprefixed ``, which carries no r:embed.
let blip_fill_body = match extract_tag_body_from(pic_body, "xdr:blipFill") {
Some(value) => value
None =>
match extract_tag_body_from(pic_body, "blipFill") {
Some(value) => value
None => continue
}
}
let blip_attrs = match tag_attributes_in(blip_fill_body, "a:blip") {
Some(value) => value
None =>
match tag_attributes_in(blip_fill_body, "blip") {
Some(value) => value
None => continue
}
}
let embed = match attr_value(blip_attrs, "r:embed") {
Some(value) => value
None =>
match attr_value(blip_attrs, "embed") {
Some(value) => value
None => continue
}
}
let target = match targets.get(embed) {
Some(value) => value
None => continue
}
let media_path = actual_relationship_target_path(
source_part,
target,
part_names,
cancelled~,
)
match archive.get(media_path) {
Some(_) => {
let (data, identity) = load_image_part(
archive,
content_types,
media_path,
"cell image",
cancelled~,
)
images.push({
name: unescape_xml_text(name),
data: data.to_owned(),
extension: "." + identity.extension,
content_type: identity.content_type,
alt_text: unescape_xml_text(alt_text),
})
}
None => continue
}
}
images
}
///|
/// Builds an `Image` positioned in `reference` from an embedded cell
/// image. Cell images have no anchor geometry, so offsets/size are zero
/// and positioning is one-cell, matching how Excelize surfaces a
/// place-in-cell `Picture`.
fn CellImage::to_image(self : CellImage, reference : String) -> Image {
{
reference,
data: self.data,
extension: self.extension,
content_type: self.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: self.name,
alt_text: self.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,
}
}
///|
/// Resolves the Kingsoft WPS Office embedded images (`DISPIMG`) shown in
/// `reference`, mirroring Excelize's `getDispImages`: a cell whose
/// formula is a `DISPIMG` call evaluates to the image identifier, which
/// selects the matching `cellimages.xml` entry.
fn Workbook::disp_images_for_cell(
self : Workbook,
sheet : Worksheet,
reference : StringView,
) -> Array[Image] raise XlsxError {
let result : Array[Image] = []
if self.cell_images.length() == 0 {
return result
}
let formula = match sheet.get_cell_formula(reference) {
Some(value) => value
None => return result
}
if !is_disp_img_formula(formula) {
return result
}
let img_id = self.calc_cell_value(sheet.name(), reference)
let canonical = {
let (row, col) = cell_ref_to_rc(reference)
cell_ref_from(row, col)
}
for image in self.cell_images {
if image.name == img_id {
result.push(image.to_image(canonical))
}
}
result
}
///|
/// Reports whether a formula is a `DISPIMG` call, tolerating a leading
/// `=` and the `_xlfn.` future-function prefix, like Excelize.
fn is_disp_img_formula(formula : String) -> Bool {
let mut text = formula
if text.has_prefix("=") {
text = text[1:].to_owned()
}
if text.has_prefix("_xlfn.") {
text = text[6:].to_owned()
}
text.has_prefix("DISPIMG")
}
///|
test "parse_cell_images handles unprefixed blip without matching blipFill" {
// an unprefixed cellimages.xml: must not be mistaken for
let cellimages_xml =
#|
#|
#|
#|
#|
#|
#|
#|
let rels_xml =
#|
#|
#|
let archive = @zip.Archive::new()
archive.add("xl/media/u.png", b"\x89PNGunprefixed")
let images = parse_cell_images(
cellimages_xml,
rels_xml,
"xl/cellimages.xml",
archive_part_name_index(archive),
image_reader_test_content_types(),
archive,
)
inspect(images.length(), content="1")
inspect(images[0].name, content="ID_U")
inspect(images[0].extension, content=".png")
}
///|
test "parse_cell_images resolves targets relative to its source part" {
let cellimages_xml =
#|
let rels_xml =
#|
let archive = @zip.Archive::new()
archive.add("decoy.png", b"root image")
archive.add("xl/decoy.png", b"wrong image")
let images = parse_cell_images(
cellimages_xml,
rels_xml,
"xl/cellimages.xml",
archive_part_name_index(archive),
image_reader_test_content_types(),
archive,
)
assert_eq(images.length(), 1)
assert_eq(images[0].data, b"root image")
}