Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
cat > build/_consumer/consumer.cpp <<'EOF'
#include <mark2haru/mark2haru.h>
int main() {
mark2haru::render_options_t options;
mark2haru::Render_options options;
return options.page_width_pt > 0.0 ? 0 : 1;
}
EOF
Expand Down
18 changes: 18 additions & 0 deletions src/io_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <iterator>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>

namespace mark2haru
Expand All @@ -22,6 +23,23 @@ inline bool read_file_bytes(const std::filesystem::path& path, std::vector<std::
return !out.empty();
}

// Reads `sizeof(T)` big-endian bytes from `p` and returns them as T. T must
// be an unsigned integer of 1, 2, 4, or 8 bytes. Callers are responsible
// for bounds-checking `p` before the call.
template <class T>
constexpr T read_be(const std::uint8_t* p) noexcept
{
static_assert(std::is_unsigned_v<T>, "read_be<T>: T must be unsigned");
static_assert(
sizeof(T) == 1 || sizeof(T) == 2 || sizeof(T) == 4 || sizeof(T) == 8,
"read_be<T>: only 8/16/32/64-bit widths are supported");
T value = 0;
for (std::size_t i = 0; i < sizeof(T); ++i) {
value = static_cast<T>((value << 8) | static_cast<T>(p[i]));
}
return value;
}

// Splits on '\n', stripping a trailing '\r' so CRLF and LF inputs produce
// the same line vector. Keeps a final empty line if the input ends with '\n'.
inline std::vector<std::string> split_lines(std::string_view text)
Expand Down
13 changes: 5 additions & 8 deletions src/pdf_writer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -240,21 +240,18 @@ std::string build_stream_object(
return out;
}

std::string build_stream_object(const std::string& payload, const std::string& extra = {})
// Catches anything with .data() and .size(): std::string, std::vector<uint8_t>,
// std::array, std::span, etc. The reinterpret_cast is a no-op for unsigned-byte
// containers and a defined conversion for std::string (char -> unsigned char).
template <class Container>
std::string build_stream_object(const Container& payload, const std::string& extra = {})
{
return build_stream_object(
reinterpret_cast<const unsigned char*>(payload.data()),
payload.size(),
extra);
}

std::string build_stream_object(
const std::vector<std::uint8_t>& payload,
const std::string& extra = {})
{
return build_stream_object(payload.data(), payload.size(), extra);
}

// Allocates 1-based PDF object IDs on demand and lets callers fill in the
// body any time before write(). Eliminates the manual "next_obj++" arithmetic
// and the implicit invariant that body order matches reservation order.
Expand Down
10 changes: 2 additions & 8 deletions src/png_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,12 @@ constexpr std::size_t k_max_idat_bytes = 256u * 1024u * 1024u; // 256 MiB

std::uint32_t read_be32(const std::vector<std::uint8_t>& bytes, std::size_t offset)
{
return
(static_cast<std::uint32_t>(bytes[offset ]) << 24) |
(static_cast<std::uint32_t>(bytes[offset + 1]) << 16) |
(static_cast<std::uint32_t>(bytes[offset + 2]) << 8) |
static_cast<std::uint32_t>(bytes[offset + 3]);
return read_be<std::uint32_t>(&bytes[offset]);
}

std::uint16_t read_be16(const std::vector<std::uint8_t>& bytes, std::size_t offset)
{
return static_cast<std::uint16_t>(
(static_cast<std::uint16_t>(bytes[offset ]) << 8) |
static_cast<std::uint16_t>(bytes[offset + 1]) );
return read_be<std::uint16_t>(&bytes[offset]);
}

// Adds two size_t values and reports overflow. Returns true on success;
Expand Down
190 changes: 15 additions & 175 deletions src/render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,185 +4,25 @@
#include <mark2haru/png_image.h>

#include "io_helpers.h"
#include "utf8_decode.h"
#include "text_layout.h"

#include <algorithm>
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>

namespace mark2haru {
namespace {

namespace fs = std::filesystem;

Pdf_font font_for(Inline_style style)
{
switch (style) {
case Inline_style::BOLD: return Pdf_font::BOLD;
case Inline_style::ITALIC: return Pdf_font::ITALIC;
case Inline_style::BOLD_ITALIC: return Pdf_font::BOLD_ITALIC;
case Inline_style::CODE: return Pdf_font::MONO;
case Inline_style::NORMAL:
default: return Pdf_font::REGULAR;
}
}

struct Token
{
std::string text;
Inline_style style = Inline_style::NORMAL;
bool newline = false;
};

struct Line
{
std::vector<std::pair<std::string, Inline_style>>
spans;
double height_pt = 0.0;
};

std::vector<Token> tokenize_runs(const std::vector<Inline_run>& runs)
{
std::vector<Token> tokens;
for (const auto& run : runs) {
size_t start = 0;
while (start <= run.text.size()) {
const size_t nl = run.text.find('\n', start);
const std::string chunk = nl == std::string::npos
? run.text.substr(start)
: run.text.substr(start, nl - start);

size_t piece = 0;
while (piece <= chunk.size()) {
const size_t sp = chunk.find(' ', piece);
if (sp == std::string::npos) {
if (piece < chunk.size()) {
tokens.push_back({ chunk.substr(piece), run.style, false });
}
break;
}
if (sp > piece) {
tokens.push_back({ chunk.substr(piece, sp - piece), run.style, false });
}
tokens.push_back({ " ", run.style, false });
piece = sp + 1;
}

if (nl == std::string::npos) {
break;
}
tokens.push_back({ "", run.style, true });
start = nl + 1;
}
}
return tokens;
}

template <class MeasureFn>
std::vector<Line> wrap_tokens(
const std::vector<Token>& tokens,
double max_width_pt,
double size_pt,
double leading,
MeasureFn&& measure)
{
std::vector<Line> lines;
Line current;
double current_width = 0.0;

auto finish_line = [&]() {
current.height_pt = size_pt * leading;
lines.push_back(current);
current = Line{};
current_width = 0.0;
};

auto add_word = [&](const std::string& word, Inline_style style) {
const double word_width = measure(font_for(style), word, size_pt);
if (current_width > 0.0 && current_width + word_width > max_width_pt) {
finish_line();
}
if (word_width <= max_width_pt) {
current.spans.emplace_back(word, style);
current_width += word_width;
return;
}

// Word is wider than a full line: break it at code-point boundaries.
std::string fragment;
double fragment_width = 0.0;
for (const auto& piece : utf8::split_pieces(word)) {
const double ch_width = measure(font_for(style), piece, size_pt);
const double projected = current_width + fragment_width + ch_width;
const bool any_content_on_line = current_width > 0.0 || fragment_width > 0.0;
if (any_content_on_line && projected > max_width_pt) {
if (!fragment.empty()) {
current.spans.emplace_back(fragment, style);
current_width += fragment_width;
fragment.clear();
fragment_width = 0.0;
}
finish_line();
}
fragment += piece;
fragment_width += ch_width;
}
if (!fragment.empty()) {
current.spans.emplace_back(fragment, style);
current_width += fragment_width;
}
};

for (const auto& token : tokens) {
if (token.newline) {
finish_line();
continue;
}
if (token.text == " ") {
const double space_width = measure(font_for(token.style), token.text, size_pt);
if (!current.spans.empty() && current_width + space_width > max_width_pt) {
finish_line();
}
else
if (!current.spans.empty()) {
current.spans.emplace_back(token.text, token.style);
current_width += space_width;
}
continue;
}
if (!token.text.empty()) {
add_word(token.text, token.style);
}
}

if (!current.spans.empty() || lines.empty()) {
finish_line();
}
return lines;
}

template <class MeasureFn>
std::vector<Line> wrap_runs(
const std::vector<Inline_run>& runs,
double max_width_pt,
double size_pt,
double leading,
MeasureFn&& measure)
{
return wrap_tokens(tokenize_runs(runs), max_width_pt, size_pt, leading, measure);
}

double total_height(const std::vector<Line>& lines)
{
double h = 0.0;
for (const auto& line : lines) {
h += line.height_pt;
}
return h;
}
using text_layout::font_for;
using text_layout::Line;
using text_layout::Token;
using text_layout::total_height;
using text_layout::wrap_tokens;
using text_layout::wrap_runs;

// Standard "overloaded" idiom to combine a set of single-type lambdas
// into one callable that std::visit can dispatch on. Adding a new block
Expand Down Expand Up @@ -239,18 +79,18 @@ std::vector<Line> code_lines(
const std::string& text,
double max_width_pt,
double size_pt,
double leading,
double line_height_pt,
MeasureFn&& measure)
{
std::vector<Line> lines;
for (const auto& raw : split_lines(text)) {
std::vector<Token> tokens;
tokens.push_back({ raw, Inline_style::CODE, false });
auto wrapped = wrap_tokens(tokens, max_width_pt, size_pt, leading, measure);
auto wrapped = wrap_tokens(tokens, max_width_pt, size_pt, line_height_pt, measure);
lines.insert(lines.end(), wrapped.begin(), wrapped.end());
}
if (lines.empty()) {
lines.push_back({ {}, size_pt * leading });
lines.push_back({ {}, line_height_pt });
}
return lines;
}
Expand Down Expand Up @@ -326,7 +166,7 @@ bool render_markdown_to_pdf(

auto on_paragraph = [&](const Paragraph_block& pb) {
const auto lines = wrap_runs(pb.runs, content_width, options.body_size_pt,
options.line_spacing, measure);
options.body_size_pt * options.line_spacing, measure);
ensure_space(total_height(lines) + options.body_size_pt * 0.25);
cursor_y = draw_lines_at(lines, options.body_size_pt, options.margin_left_pt, cursor_y);
cursor_y += options.body_size_pt * 0.35;
Expand All @@ -337,7 +177,7 @@ bool render_markdown_to_pdf(
const double size = options.body_size_pt * hm.size_factor;
const double space_before = size * hm.space_before_factor;
const double space_after = size * hm.space_after_factor;
const auto lines = wrap_runs(hb.runs, content_width, size, 1.15, measure);
const auto lines = wrap_runs(hb.runs, content_width, size, size * 1.15, measure);
ensure_space(space_before + total_height(lines) + space_after);
cursor_y += space_before;
cursor_y = draw_lines_at(lines, size, options.margin_left_pt, cursor_y);
Expand All @@ -355,7 +195,7 @@ bool render_markdown_to_pdf(
lb.items[i].runs,
item_width,
options.body_size_pt,
options.line_spacing,
options.body_size_pt * options.line_spacing,
measure);
ensure_space(total_height(lines) + options.body_size_pt * 0.2);
writer.draw_text(
Expand Down Expand Up @@ -407,7 +247,7 @@ bool render_markdown_to_pdf(
const double size = options.body_size_pt * 0.92;
const double pad = options.body_size_pt * 0.45;
const double available_width = content_width - pad * 2.0;
const auto lines = code_lines(cb.text, available_width, size, 1.25, measure);
const auto lines = code_lines(cb.text, available_width, size, size * 1.25, measure);
const double height = total_height(lines) + pad * 2.0;
ensure_space(height + options.body_size_pt * 0.2);
writer.fill_rect(options.margin_left_pt, cursor_y, content_width, height,
Expand Down Expand Up @@ -444,7 +284,7 @@ bool render_markdown_to_pdf(
for (size_t col = 0; col < column_count; ++col) {
const std::vector<Inline_run> empty;
const auto& runs = col < row.cells.size() ? row.cells[col].runs : empty;
cell_lines[r][col] = wrap_runs(runs, inner_width, cell_size, 1.22, measure);
cell_lines[r][col] = wrap_runs(runs, inner_width, cell_size, cell_size * 1.22, measure);
const double cell_height = total_height(cell_lines[r][col]) + cell_pad * 2.0;
row_height = std::max(row_height, cell_height);
}
Expand Down
Loading
Loading