tiny.pdf.cmap
Defined in tiny.pdf.
API (6)
Actions
Public operations.
Types and contracts
Public types and contracts.
Source
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.
lib.pdf.src.cmap.deriveSpaces[function] — private source atlib/pdf/src/cmap.zig:158in nearest public ownertiny.pdf.cmaplib.pdf.src.cmap.parseChars[function] — private source atlib/pdf/src/cmap.zig:182in nearest public ownertiny.pdf.cmaplib.pdf.src.cmap.parseRanges[function] — private source atlib/pdf/src/cmap.zig:198in nearest public ownertiny.pdf.cmaplib.pdf.src.cmap.parseSpaces[function] — private source atlib/pdf/src/cmap.zig:170in nearest public ownertiny.pdf.cmaptiny.pdf.object.Parser.init[function] atlib/pdf/src/object.zig:65tiny.pdf.object.Parser.parseValue[method] atlib/pdf/src/object.zig:86tiny.pdf.object.Parser.skipWhitespace[method] atlib/pdf/src/object.zig:69tiny.pdf.object.delimiterOrWhitespace[function] atlib/pdf/src/object.zig:301
Audit
| Definitions | 7 |
|---|---|
| Public names | 7 |
| Members | 11 |
| Version | 26.7.0 |
| Revision | daab053ee433 |