tiny.pdf.content
Defined in tiny.pdf.
API (1)
Actions
Public operations.
Source
Source: lib/pdf/src/content.zig
zig
const std = @import("std");const alloc_arena = @import("alloc_arena");const font = @import("font.zig");const object = @import("object.zig");const Piece = union(enum) { text: object.String, space,};pub fn extractTextAppend(allocator: std.mem.Allocator, out: *std.ArrayList(u8), content: []const u8, fonts: font.Set) !void { var parser = object.Parser.init(content, 0); var pending_strings: std.ArrayList(Piece) = .empty; defer pending_strings.deinit(allocator); var arena_state = alloc_arena.Arena.init(allocator); defer arena_state.deinit(); const arena = arena_state.allocator(); var last_name: ?[]const u8 = null; var current_font: ?*const font.Font = null; for (content) |_| { parser.skipWhitespace(); if (parser.pos >= content.len) break; const byte = content[parser.pos]; if (byte == '/' or byte == '(' or byte == '<' or byte == '[' or std.ascii.isDigit(byte) or byte == '+' or byte == '-' or byte == '.') { const value = parser.parseValue(arena) catch { parser.pos += 1; continue; }; switch (value) { .name => |name| last_name = name, else => {}, } try collectStrings(allocator, &pending_strings, value); continue; } const start = parser.pos; while (parser.pos < content.len and !object.delimiterOrWhitespace(content[parser.pos])) parser.pos += 1; if (parser.pos == start) { parser.pos += 1; continue; } const operator = content[start..parser.pos]; if (std.mem.eql(u8, operator, "Tf")) { current_font = if (last_name) |name| fonts.get(name) else null; } try applyOperator(allocator, out, &pending_strings, operator, current_font); }}fn collectStrings(allocator: std.mem.Allocator, pending: *std.ArrayList(Piece), value: object.Value) !void { switch (value) { .string => |string| try pending.append(allocator, .{ .text = string }), .array => |items| { for (items) |item| { switch (item) { .string => |string| try pending.append(allocator, .{ .text = string }), .integer => |kern| if (kern <= -space_kerning_threshold) try pending.append(allocator, .space), .real => |kern| if (kern <= -@as(f64, space_kerning_threshold)) try pending.append(allocator, .space), else => {}, } } }, else => {}, }}const space_kerning_threshold = 180;fn applyOperator(allocator: std.mem.Allocator, out: *std.ArrayList(u8), pending: *std.ArrayList(Piece), operator: []const u8, current_font: ?*const font.Font) !void { if (std.mem.eql(u8, operator, "Tj") or std.mem.eql(u8, operator, "'") or std.mem.eql(u8, operator, "\"") or std.mem.eql(u8, operator, "TJ")) { if (std.mem.eql(u8, operator, "'") or std.mem.eql(u8, operator, "\"")) { try breakLine(allocator, out); } for (pending.items) |piece| { switch (piece) { .text => |string| try appendString(allocator, out, string, current_font), .space => { if (out.items.len > 0 and out.items[out.items.len - 1] != ' ' and out.items[out.items.len - 1] != '\n') { try out.append(allocator, ' '); } }, } } pending.clearRetainingCapacity(); return; } if (std.mem.eql(u8, operator, "Td") or std.mem.eql(u8, operator, "TD") or std.mem.eql(u8, operator, "T*") or std.mem.eql(u8, operator, "ET")) { try breakLine(allocator, out); pending.clearRetainingCapacity(); return; } pending.clearRetainingCapacity();}fn appendString(allocator: std.mem.Allocator, out: *std.ArrayList(u8), string: object.String, current_font: ?*const font.Font) !void { var raw = std.ArrayList(u8).empty; defer raw.deinit(allocator); switch (string.kind) { .literal => try object.decodeLiteralAppend(allocator, &raw, string.raw), .hex => try object.decodeHexAppend(allocator, &raw, string.raw), } try font.appendText(current_font, allocator, out, raw.items);}fn breakLine(allocator: std.mem.Allocator, out: *std.ArrayList(u8)) !void { if (out.items.len == 0) return; if (out.items[out.items.len - 1] == '\n') return; try out.append(allocator, '\n');}test "content extraction reads show operators lines and tj arrays" { var out = std.ArrayList(u8).empty; defer out.deinit(std.testing.allocator); const content = "BT /F1 12 Tf 72 700 Td (Hello, ) Tj (world) Tj T* (second line) Tj " ++ "0 -14 Td [(kerned) -250 (array) 12 (Close)] TJ ET"; try extractTextAppend(std.testing.allocator, &out, content, .{}); try std.testing.expectEqualStrings("Hello, world\nsecond line\nkerned arrayClose\n", out.items);}test "content extraction survives unknown operators and hex strings" { var out = std.ArrayList(u8).empty; defer out.deinit(std.testing.allocator); try extractTextAppend(std.testing.allocator, &out, "q 1 0 0 1 10 10 cm BT <48690A> Tj ET Q", .{}); try std.testing.expectEqualStrings("Hi\n", out.items);}test "content extraction maps codes through the selected font" { const cmap = @import("cmap.zig"); var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena_state.deinit(); const map = try cmap.parseAlloc( arena_state.allocator(), "1 begincodespacerange <00> <FF> endcodespacerange " ++ "1 beginbfrange <20> <7E> <0020> endbfrange " ++ "1 beginbfchar <02> <00660069> endbfchar", ); const mapped = font.Font{ .map = map }; const plain = font.Font{}; const set = font.Set{ .entries = &.{ .{ .name = "F1", .font = &mapped }, .{ .name = "F2", .font = &plain }, } }; var out = std.ArrayList(u8).empty; defer out.deinit(std.testing.allocator); try extractTextAppend( std.testing.allocator, &out, "BT /F1 9 Tf (Arti\\002cial) Tj /F2 9 Tf ( raw\\002) Tj ET", set, ); try std.testing.expectEqualStrings("Artificial raw\x02\n", out.items);}Source: lib/pdf/src/root.zig:12
zig
pub const content = @import("content.zig");Audit
| Definitions | 2 |
|---|---|
| Public names | 2 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |