Skip to content
Merged
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
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ Dispatch `release.yml` against main, publish the draft that appears β€”
input (`std::runtime_error` or the typed exceptions in `src/odr/exceptions.hpp`)
rather than silently degrading. Only pass through (return empty / skip) values
that are genuinely *optional* or *not yet modelled*.
- **Format numbers with `fmt`, never a stream**: a stream carries the global
locale the host sets, and a german one writes `1,5` into a css length.
`util::number::to_string_significant` is the css/svg spelling. **Not
`std::format`** β€” libc++ reaches it through a floating-point `std::to_chars`
unavailable before macOS 13.3 / iOS 16.3, and the apple slices deploy to
macOS 12 / iOS 15, so one call anywhere in `src/` fails the framework build.
Only `-mmacosx-version-min=12.0` shows it.
- **Fixed-width integer types β€” always**: prefer `<cstdint>` types (`std::int32_t`,
`std::uint8_t`, …) over `int` / `unsigned` / `long` / `unsigned char`. Reserve
the built-in types for genuinely index/size-like values (`std::size_t`) or where
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- Fixed: a semi-transparent colour wrote `rgba(0,0,0,0,501961)` where the host
had set a global locale with a comma decimal separator. Adds a `fmt`
dependency.

- New `Sheet::page_layout()`: the paper an ods states for a sheet, read from
the master page its table style names. Mirrored in the Python, JNI and Apple
bindings. Empty for xlsx, xls, numbers and csv.
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ set_property(TARGET pugixml::pugixml APPEND PROPERTY
find_package(md4c REQUIRED)
find_package(miniz REQUIRED)
find_package(cryptopp REQUIRED)
find_package(fmt REQUIRED)
find_package(nlohmann_json REQUIRED)
find_package(OpenJPEG REQUIRED)
find_package(uchardet REQUIRED)
Expand Down Expand Up @@ -318,6 +319,7 @@ target_link_libraries(odr
md4c::md4c
miniz::miniz
cryptopp::cryptopp
fmt::fmt
nlohmann_json::nlohmann_json
openjp2
uchardet::uchardet
Expand Down
1 change: 1 addition & 0 deletions conan.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"miniz/3.1.1#ae48ea4c4ee773386b496b9722e3b66a%1778764964.632",
"md4c/0.5.2#3d7106721e458f9f799b87d4d50d02e0%1746796758.933",
"gtest/1.14.0#f8f0757a574a8dd747d16af62d6eb1b7%1743410807.169",
"fmt/11.2.0#15c2efeeecd75ba2fb9dafbec8dfa4cc%1785754557.243",
"cryptopp/8.9.0#7a51e0038756b21bc3a6b82d681d5906%1758206597.119",
"cpp-httplib/0.47.0#add6673ff352c26898ed2650453e706e%1784539639.401",
"bzip2/1.0.8#c470882369c2d95c5c77e970c0c7e321%1762886692.465"
Expand Down
1 change: 1 addition & 0 deletions conanfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def config_options(self):
def requirements(self):
self.requires("pugixml/1.15")
self.requires("cryptopp/8.9.0")
self.requires("fmt/11.2.0")
self.requires("md4c/0.5.2")
self.requires("miniz/3.1.1")
self.requires("nlohmann_json/3.12.0")
Expand Down
21 changes: 9 additions & 12 deletions src/odr/internal/html/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
#include <cctype>
#include <cmath>
#include <cstdint>

#include <fmt/format.h>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_map>
Expand Down Expand Up @@ -320,17 +320,14 @@ html::fill_path_variables(const std::string &path,

std::string html::color(const Color &color) {
if (color.alpha != 255) {
std::stringstream ss;
ss << "rgba(" << static_cast<std::uint32_t>(color.red) << ","
<< static_cast<std::uint32_t>(color.green) << ","
<< static_cast<std::uint32_t>(color.blue) << ","
<< (static_cast<double>(color.alpha) / 255.0) << ")";
return ss.str();
// `{:g}` is the six significant digits the stream this replaced wrote.
return fmt::format("rgba({},{},{},{:g})",
static_cast<std::uint32_t>(color.red),
static_cast<std::uint32_t>(color.green),
static_cast<std::uint32_t>(color.blue),
static_cast<double>(color.alpha) / 255.0);
}
std::stringstream ss;
ss << "#";
ss << std::setw(6) << std::setfill('0') << std::hex << color.rgb();
return ss.str();
return fmt::format("#{:06x}", color.rgb());
}

std::string html::file_to_url(const std::string &file,
Expand Down
9 changes: 3 additions & 6 deletions src/odr/internal/html/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
#include <odr/internal/xml/xml_util.hpp>

#include <array>
#include <iomanip>

#include <fmt/format.h>
#include <mutex>
#include <sstream>

namespace odr::internal::html {
namespace {
Expand All @@ -36,10 +36,7 @@ std::string human_size(const std::size_t size) {
++unit;
}

std::ostringstream result;
result << std::fixed << std::setprecision(unit == 0 ? 0 : 1) << value << " "
<< units.at(unit);
return result.str();
return fmt::format("{:.{}f} {}", value, unit == 0 ? 0 : 1, units.at(unit));
}

/// From the extension, not the bytes: sniffing every entry would read the whole
Expand Down
6 changes: 4 additions & 2 deletions src/odr/internal/pdf/pdf_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
#include <odr/internal/crypto/crypto_util.hpp>
#include <odr/internal/util/hash_util.hpp>

#include <iomanip>
#include <optional>
#include <ostream>
#include <sstream>

#include <fmt/format.h>
#include <stdexcept>

namespace odr::internal::pdf {
Expand Down Expand Up @@ -135,7 +136,8 @@ void Object::to_stream(std::ostream &out) const {
} else if (is_integer()) {
out << as_integer();
} else if (is_real()) {
out << std::setprecision(4) << as_real();
// not `setprecision`, which would stick to the stream
out << fmt::format("{:.4g}", as_real());
} else if (is_standard_string()) {
as<StandardString>().to_stream(out);
} else if (is_hex_string()) {
Expand Down
19 changes: 6 additions & 13 deletions src/odr/internal/util/number_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,27 @@

#include <algorithm>
#include <cmath>
#include <iomanip>
#include <locale>
#include <sstream>

#include <fmt/format.h>

namespace odr::internal::util {

std::string number::to_string_significant(const double value,
const int significant_digits) {
if (!std::isfinite(value)) {
std::ostringstream ss;
ss.imbue(std::locale::classic());
ss << value;
return ss.str();
return fmt::format("{}", value);
}

// `std::fixed` counts decimals, not significant digits, so shift by the
// integer part; clamped because a denormal or a huge value would blow up
// `{:.Nf}` counts decimals, not significant digits, so shift by the integer
// part; clamped because a denormal or a huge value would blow up
int integer_digits = 1;
if (value != 0.0) {
integer_digits =
static_cast<int>(std::floor(std::log10(std::abs(value)))) + 1;
}
const int decimals = std::clamp(significant_digits - integer_digits, 0, 15);

std::ostringstream ss;
ss.imbue(std::locale::classic());
ss << std::fixed << std::setprecision(decimals) << value;
std::string result = ss.str();
std::string result = fmt::format("{:.{}f}", value, decimals);

if (result.find('.') != std::string::npos) {
result.erase(result.find_last_not_of('0') + 1);
Expand Down
2 changes: 1 addition & 1 deletion src/odr/internal/util/number_util.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace odr::internal::util::number {

/// Renders @p value with @p significant_digits significant digits, without
/// trailing zeros, never in scientific notation, which CSS and SVG lengths do
/// not accept, and in the classic locale, where a german one would write
/// not accept, and never in the host's locale, where a german one would write
/// `1,5`. Asking for more digits than the source has shows its noise: a
/// `float` carries about 7, beyond that `68.55` becomes `68.550003`.
std::string to_string_significant(double value, int significant_digits);
Expand Down
8 changes: 3 additions & 5 deletions src/odr/internal/util/string_util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <stdexcept>

#include <fmt/format.h>

#include <utf8/unchecked.h>
#include <utf8cpp/utf8/checked.h>
#include <utf8cpp/utf8/cpp17.h>
Expand Down Expand Up @@ -167,9 +167,7 @@ std::vector<std::string> string::split(const std::string &string,
}

std::string string::to_string(const double d, const int precision) {
std::stringstream stream;
stream << std::fixed << std::setprecision(precision) << d;
return stream.str();
return fmt::format("{:.{}f}", d, precision);
}

std::size_t string::utf8_length(const std::string &string) {
Expand Down
29 changes: 29 additions & 0 deletions test/src/internal/html/common_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <gtest/gtest.h>

#include <limits>
#include <locale>
#include <optional>
#include <sstream>
#include <string>
Expand Down Expand Up @@ -355,3 +356,31 @@ TEST(html_common, whitespace_inside_a_scheme_does_not_hide_it) {
ihtml::is_safe_uri(std::string("javascript") + '\0' + ":alert(1)"));
EXPECT_FALSE(ihtml::is_safe_uri("\x01javascript:alert(1)"));
}

namespace {

class LocaleGuard final {
public:
~LocaleGuard() { std::locale::global(m_previous); }

private:
std::locale m_previous{std::locale()};
};

} // namespace

/// The alpha is the only number `color` prints, so it is the only way the
/// host's locale could reach the css. `rgba(0,0,0,0,501961)` is not a colour.
TEST(html_common, a_global_locale_does_not_reach_the_css) {
const Color translucent{0, 0, 0, 128};
ASSERT_EQ(ihtml::color(translucent), "rgba(0,0,0,0.501961)");

const LocaleGuard guard;
try {
std::locale::global(std::locale("de_DE.UTF-8"));
} catch (const std::runtime_error &) {
GTEST_SKIP() << "de_DE.UTF-8 is not installed";
}

EXPECT_EQ(ihtml::color(translucent), "rgba(0,0,0,0.501961)");
}
Loading