Skip to documentation
SLOP

tiny.pdf.cmap

Reference tiny.pdf cmap

Defined in tiny.pdf.

API (6)

Actions

Public operations.

Types and contracts

Public types and contracts.

No direct callersNo direct callstiny.pdfcmap
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsNo direct callersprivate sourcelib.pdf.src.cmap.MapappendCodeprivate sourcelib.pdf.src.cmap.MaptakeCodeprivate sourcelib.pdf.src.cmapappendReplacementcmap.MapappendDecoded
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallstest sourcelib.pdf.src.cmaptest: cmap parses tex shaped tounicod...test sourcelib.pdf.src.cmaptest: cmap stepped ranges honor multi...test sourcelib.pdf.src.cmaptest: cmap surrogate pairs and empty ...test sourcelib.pdf.src.contenttest: content extraction maps codes t...private sourcelib.pdf.src.document.DocumentfontMapAllocprivate sourcelib.pdf.src.cmapderiveSpacesprivate sourcelib.pdf.src.cmapparseCharsprivate sourcelib.pdf.src.cmapparseRangesprivate sourcelib.pdf.src.cmapparseSpacesobject.Parserinit+3 morecmapparseAlloc
Static calls · unresolved targets: 2 · external targets: 0.

Source: lib/pdf/src/cmap.zig

zig
const std = @import("std");const object = @import("object.zig");pub const ParseError = error{    BadCMap,    OutOfMemory,};pub const Space = struct {    width: u8,    low: u32,    high: u32,};const Code = struct {    width: u8,    value: u32,};const SteppedMapping = struct {    prefix: []const u8,    base: u21,};const MappedPayload = union(enum) {    single: []const u8,    stepped: SteppedMapping,    listed: []const []const u8,};pub const Mapped = struct {    width: u8,    low: u32,    high: u32,    payload: MappedPayload,};pub const Map = struct {    spaces: []const Space,    ranges: []const Mapped,    pub fn appendDecoded(self: Map, allocator: std.mem.Allocator, out: *std.ArrayList(u8), bytes: []const u8) error{OutOfMemory}!void {        var pos: usize = 0;        for (bytes) |_| {            if (pos >= bytes.len) break;            const code = self.takeCode(bytes, &pos) orelse {                pos += 1;                try appendReplacement(allocator, out);                continue;            };            try self.appendCode(allocator, out, code);        }    }    fn takeCode(self: Map, bytes: []const u8, pos: *usize) ?Code {        var width: u8 = 1;        while (width <= 4) : (width += 1) {            if (pos.* + width > bytes.len) return null;            const value = readCode(bytes[pos.* .. pos.* + width]);            if (self.spaceContains(width, value)) {                pos.* += width;                return .{ .width = width, .value = value };            }        }        return null;    }    fn spaceContains(self: Map, width: u8, value: u32) bool {        for (self.spaces) |space| {            if (space.width == width and value >= space.low and value <= space.high) return true;        }        return false;    }    fn appendCode(self: Map, allocator: std.mem.Allocator, out: *std.ArrayList(u8), code: Code) error{OutOfMemory}!void {        for (self.ranges) |range| {            if (range.width != code.width or code.value < range.low or code.value > range.high) continue;            const delta = code.value - range.low;            switch (range.payload) {                .single => |text| {                    if (delta != 0) break;                    out.appendSlice(allocator, text) catch return error.OutOfMemory;                    return;                },                .stepped => |stepped| {                    out.appendSlice(allocator, stepped.prefix) catch return error.OutOfMemory;                    const scalar = @as(u32, stepped.base) + delta;                    try appendScalar(allocator, out, scalar);                    return;                },                .listed => |texts| {                    if (delta >= texts.len) break;                    out.appendSlice(allocator, texts[delta]) catch return error.OutOfMemory;                    return;                },            }        }        try appendReplacement(allocator, out);    }};fn readCode(bytes: []const u8) u32 {    var value: u32 = 0;    for (bytes) |byte| value = (value << 8) | byte;    return value;}fn appendScalar(allocator: std.mem.Allocator, out: *std.ArrayList(u8), scalar: u32) error{OutOfMemory}!void {    const valid = scalar <= 0x10FFFF and !(scalar >= 0xD800 and scalar <= 0xDFFF);    if (!valid) return appendReplacement(allocator, out);    var buffer: [4]u8 = undefined;    const len = std.unicode.utf8Encode(@intCast(scalar), &buffer) catch return appendReplacement(allocator, out);    out.appendSlice(allocator, buffer[0..len]) catch return error.OutOfMemory;}fn appendReplacement(allocator: std.mem.Allocator, out: *std.ArrayList(u8)) error{OutOfMemory}!void {    out.appendSlice(allocator, "\u{FFFD}") catch return error.OutOfMemory;}pub fn parseAlloc(arena: std.mem.Allocator, bytes: []const u8) ParseError!Map {    var parser = object.Parser.init(bytes, 0);    var spaces: std.ArrayList(Space) = .empty;    var ranges: std.ArrayList(Mapped) = .empty;    for (bytes) |_| {        parser.skipWhitespace();        if (parser.pos >= bytes.len) break;        const byte = bytes[parser.pos];        if (byte == '<' or byte == '[' or byte == '(' or byte == '/' or std.ascii.isDigit(byte) or byte == '+' or byte == '-' or byte == '.') {            _ = parser.parseValue(arena) catch {                parser.pos += 1;            };            continue;        }        const start = parser.pos;        while (parser.pos < bytes.len and !object.delimiterOrWhitespace(bytes[parser.pos])) parser.pos += 1;        if (parser.pos == start) {            parser.pos += 1;            continue;        }        const keyword = bytes[start..parser.pos];        if (std.mem.eql(u8, keyword, "begincodespacerange")) {            try parseSpaces(arena, &parser, &spaces);        } else if (std.mem.eql(u8, keyword, "beginbfchar")) {            try parseChars(arena, &parser, &ranges);        } else if (std.mem.eql(u8, keyword, "beginbfrange")) {            try parseRanges(arena, &parser, &ranges);        }    }    if (ranges.items.len == 0) return error.BadCMap;    if (spaces.items.len == 0) try deriveSpaces(arena, ranges.items, &spaces);    return .{        .spaces = spaces.toOwnedSlice(arena) catch return error.OutOfMemory,        .ranges = ranges.toOwnedSlice(arena) catch return error.OutOfMemory,    };}fn deriveSpaces(arena: std.mem.Allocator, ranges: []const Mapped, spaces: *std.ArrayList(Space)) ParseError!void {    var widths = @as([5]bool, @splat(false));    for (ranges) |range| widths[range.width] = true;    for (widths, 0..) |present, width| {        if (!present or width == 0) continue;        const high: u32 = if (width >= 4) std.math.maxInt(u32) else (@as(u32, 1) << @intCast(width * 8)) - 1;        spaces.append(arena, .{ .width = @intCast(width), .low = 0, .high = high }) catch return error.OutOfMemory;    }}const entry_cap = 1 << 16;fn parseSpaces(arena: std.mem.Allocator, parser: *object.Parser, spaces: *std.ArrayList(Space)) ParseError!void {    for (parser.bytes[parser.pos..]) |_| {        if (spaces.items.len >= entry_cap) return;        const low = nextHexToken(arena, parser, "endcodespacerange") orelse return;        const high = nextHexToken(arena, parser, "endcodespacerange") orelse return;        const low_code = hexCode(low) orelse continue;        const high_code = hexCode(high) orelse continue;        if (low_code.width != high_code.width) continue;        spaces.append(arena, .{ .width = low_code.width, .low = low_code.value, .high = high_code.value }) catch return error.OutOfMemory;    }}fn parseChars(arena: std.mem.Allocator, parser: *object.Parser, ranges: *std.ArrayList(Mapped)) ParseError!void {    for (parser.bytes[parser.pos..]) |_| {        if (ranges.items.len >= entry_cap) return;        const src = nextHexToken(arena, parser, "endbfchar") orelse return;        const dst = nextHexToken(arena, parser, "endbfchar") orelse return;        const src_code = hexCode(src) orelse continue;        const text = utf8FromHexUtf16Alloc(arena, dst) orelse continue;        ranges.append(arena, .{            .width = src_code.width,            .low = src_code.value,            .high = src_code.value,            .payload = .{ .single = text },        }) catch return error.OutOfMemory;    }}fn parseRanges(arena: std.mem.Allocator, parser: *object.Parser, ranges: *std.ArrayList(Mapped)) ParseError!void {    for (parser.bytes[parser.pos..]) |_| {        if (ranges.items.len >= entry_cap) return;        const low = nextHexToken(arena, parser, "endbfrange") orelse return;        const high = nextHexToken(arena, parser, "endbfrange") orelse return;        parser.skipWhitespace();        if (parser.pos >= parser.bytes.len) return;        const low_code = hexCode(low) orelse return;        const high_code = hexCode(high) orelse return;        if (parser.bytes[parser.pos] == '[') {            const value = parser.parseValue(arena) catch return;            const items = switch (value) {                .array => |array| array,                else => continue,            };            if (low_code.width != high_code.width or high_code.value < low_code.value) continue;            const texts = arena.alloc([]const u8, items.len) catch return error.OutOfMemory;            for (items, 0..) |item, index| {                texts[index] = switch (item) {                    .string => |string| utf8FromHexUtf16Alloc(arena, string) orelse "\u{FFFD}",                    else => "\u{FFFD}",                };            }            ranges.append(arena, .{                .width = low_code.width,                .low = low_code.value,                .high = high_code.value,                .payload = .{ .listed = texts },            }) catch return error.OutOfMemory;            continue;        }        const dst = nextHexToken(arena, parser, "endbfrange") orelse return;        if (low_code.width != high_code.width or high_code.value < low_code.value) continue;        try appendSteppedRange(arena, ranges, low_code, high_code, dst);    }}fn appendSteppedRange(arena: std.mem.Allocator, ranges: *std.ArrayList(Mapped), low: Code, high: Code, dst: object.String) ParseError!void {    var buffer: [64]u8 = undefined;    const raw_len = hexToBytes(dst.raw, &buffer) orelse return;    var scalars: [32]u21 = undefined;    const count = utf16BeScalars(buffer[0..raw_len], &scalars) orelse return;    if (count == 0) return;    if (low.value == high.value) {        const text = utf8Alloc(arena, scalars[0..count]) orelse return;        ranges.append(arena, .{            .width = low.width,            .low = low.value,            .high = high.value,            .payload = .{ .single = text },        }) catch return error.OutOfMemory;        return;    }    const prefix = utf8Alloc(arena, scalars[0 .. count - 1]) orelse return;    ranges.append(arena, .{        .width = low.width,        .low = low.value,        .high = high.value,        .payload = .{ .stepped = .{ .prefix = prefix, .base = scalars[count - 1] } },    }) catch return error.OutOfMemory;}fn nextHexToken(arena: std.mem.Allocator, parser: *object.Parser, comptime end_keyword: []const u8) ?object.String {    parser.skipWhitespace();    if (parser.pos >= parser.bytes.len) return null;    if (parser.bytes[parser.pos] != '<') {        if (parser.atKeyword(end_keyword)) {            parser.pos += end_keyword.len;        }        return null;    }    const value = parser.parseValue(arena) catch return null;    return switch (value) {        .string => |string| if (string.kind == .hex) string else null,        else => null,    };}fn hexCode(string: object.String) ?Code {    var buffer: [4]u8 = undefined;    const len = hexToBytes(string.raw, &buffer) orelse return null;    if (len == 0) return null;    return .{ .width = @intCast(len), .value = readCode(buffer[0..len]) };}fn hexToBytes(raw: []const u8, buffer: []u8) ?usize {    var count: usize = 0;    var high: ?u8 = null;    for (raw) |char| {        const nibble: u8 = switch (char) {            '0'...'9' => char - '0',            'a'...'f' => char - 'a' + 10,            'A'...'F' => char - 'A' + 10,            ' ', '\t', '\r', '\n', 0, 0x0C => continue,            else => return null,        };        if (high) |value| {            if (count >= buffer.len) return null;            buffer[count] = (value << 4) | nibble;            count += 1;            high = null;        } else {            high = nibble;        }    }    if (high) |value| {        if (count >= buffer.len) return null;        buffer[count] = value << 4;        count += 1;    }    return count;}fn utf16BeScalars(bytes: []const u8, scalars: []u21) ?usize {    if (bytes.len % 2 != 0) return null;    var count: usize = 0;    var index: usize = 0;    while (index < bytes.len) : (index += 2) {        const unit = (@as(u16, bytes[index]) << 8) | bytes[index + 1];        if (count >= scalars.len) return null;        if (unit >= 0xD800 and unit <= 0xDBFF) {            if (index + 3 >= bytes.len) return null;            const low = (@as(u16, bytes[index + 2]) << 8) | bytes[index + 3];            if (low < 0xDC00 or low > 0xDFFF) return null;            scalars[count] = 0x10000 + (@as(u21, unit - 0xD800) << 10) + (low - 0xDC00);            index += 2;        } else if (unit >= 0xDC00 and unit <= 0xDFFF) {            return null;        } else {            scalars[count] = unit;        }        count += 1;    }    return count;}fn utf8Alloc(arena: std.mem.Allocator, scalars: []const u21) ?[]const u8 {    var out: std.ArrayList(u8) = .empty;    for (scalars) |scalar| {        var buffer: [4]u8 = undefined;        const len = std.unicode.utf8Encode(scalar, &buffer) catch return null;        out.appendSlice(arena, buffer[0..len]) catch return null;    }    return out.toOwnedSlice(arena) catch null;}fn utf8FromHexUtf16Alloc(arena: std.mem.Allocator, string: object.String) ?[]const u8 {    var buffer: [64]u8 = undefined;    const raw_len = hexToBytes(string.raw, &buffer) orelse return null;    var scalars: [32]u21 = undefined;    const count = utf16BeScalars(buffer[0..raw_len], &scalars) orelse return null;    return utf8Alloc(arena, scalars[0..count]);}const test_cmap =    "/CIDInit /ProcSet findresource begin\n" ++    "12 dict begin\nbegincmap\n" ++    "/CIDSystemInfo << /Registry (TeX) /Ordering (x) /Supplement 0 >> def\n" ++    "1 begincodespacerange\n<00> <FF>\nendcodespacerange\n" ++    "2 beginbfrange\n<61> <7A> <0061>\n<20> <26> <0020>\nendbfrange\n" ++    "2 beginbfchar\n<02> <00660069>\n<14> <006600660069>\nendbfchar\n" ++    "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n";test "cmap parses tex shaped tounicode and decodes codes" {    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena_state.deinit();    const map = try parseAlloc(arena_state.allocator(), test_cmap);    var out = std.ArrayList(u8).empty;    defer out.deinit(std.testing.allocator);    try map.appendDecoded(std.testing.allocator, &out, "Arti\x02cial \x14x");    try std.testing.expectEqualStrings("\u{FFFD}rtificial ffix", out.items);}test "cmap stepped ranges honor multi unit prefixes and lists" {    const body =        "1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n" ++        "2 beginbfrange\n<0005> <0007> <00660066006C>\n<0010> <0011> [<0041> <00420043>]\nendbfrange\n";    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena_state.deinit();    const map = try parseAlloc(arena_state.allocator(), body);    var out = std.ArrayList(u8).empty;    defer out.deinit(std.testing.allocator);    try map.appendDecoded(std.testing.allocator, &out, &.{ 0, 6, 0, 0x10, 0, 0x11, 0, 0x12 });    try std.testing.expectEqualStrings("ffmABC\u{FFFD}", out.items);}test "cmap surrogate pairs and empty maps stay honest" {    const body = "1 beginbfchar\n<01> <D83DDE00>\nendbfchar\n";    var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);    defer arena_state.deinit();    const map = try parseAlloc(arena_state.allocator(), body);    var out = std.ArrayList(u8).empty;    defer out.deinit(std.testing.allocator);    try map.appendDecoded(std.testing.allocator, &out, "\x01");    try std.testing.expectEqualStrings("\u{1F600}", out.items);    try std.testing.expectError(error.BadCMap, parseAlloc(arena_state.allocator(), "nothing here"));    const malformed =        "junk 2 begincodespacerange <GG> <HH> <00> <FF> endcodespacerange " ++        "2 beginbfchar <GG> <0042> <01> <0041> endbfchar";    const bounded = try parseAlloc(arena_state.allocator(), malformed);    out.clearRetainingCapacity();    try bounded.appendDecoded(std.testing.allocator, &out, "\x01");    try std.testing.expectEqualStrings("A", out.items);}

Source: lib/pdf/src/root.zig:11

zig
pub const cmap = @import("cmap.zig");

Complete call list for cmap.parseAlloc

8 direct calls.

Audit

Definitions7
Public names7
Members11
Version26.7.0
Revisiondaab053ee433