功能: 服务端生成完整报告并保护单任务

- 使用 libxlsxwriter 常量内存导出 Excel\n- 增加全局任务占用提示与原始数据持久化\n- 保留运行时任务数据并更新接口文档
This commit is contained in:
cloud
2026-08-05 16:47:31 +08:00
parent 07274797af
commit 99ed321d24
2292 changed files with 134317 additions and 126 deletions
+8
View File
@@ -36,6 +36,13 @@ endif()
find_package(Drogon CONFIG REQUIRED)
find_package(Threads REQUIRED)
# libxlsxwriter 使用常量内存模式把大型工作簿逐行落盘,避免报告导出占满服务进程内存。
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
add_subdirectory(${REPO_ROOT}/third_party/libxlsxwriter_repo
${CMAKE_CURRENT_BINARY_DIR}/libxlsxwriter)
file(GLOB_RECURSE SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
@@ -54,6 +61,7 @@ target_include_directories(wind_server PRIVATE
target_link_libraries(wind_server PRIVATE
Drogon::Drogon
Threads::Threads
xlsxwriter
)
# 运行期 .so 解析:RPATH 指向 Drogon install 的 libs 目录
+460 -18
View File
@@ -9,6 +9,7 @@
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <mutex>
#include <optional>
#include <random>
#include <set>
@@ -17,6 +18,11 @@
#include <unordered_map>
#include <vector>
#include <xlsxwriter/format.h>
#include <xlsxwriter/workbook.h>
#include <xlsxwriter/worksheet.h>
#include <drogon/utils/Utilities.h>
using json = nlohmann::json;
namespace fs = std::filesystem;
@@ -25,6 +31,9 @@ namespace {
constexpr int kErrorInvalidRequest = 1001;
constexpr int kErrorJobNotFound = 1002;
constexpr int kErrorServer = 1003;
constexpr int kErrorJobBusy = 1004;
constexpr auto kUploadIdleTimeout = std::chrono::minutes(30);
constexpr auto kCompletedJobRetention = std::chrono::hours(24);
constexpr const char* kDefaultSchemeId = "scheme_one";
constexpr const char* kSchemeTwoId = "scheme_two";
constexpr const char* kSchemeOneName = "方案一";
@@ -65,15 +74,17 @@ struct RemovedPoint {
struct CalculationOptions {
std::string scheme_id = kDefaultSchemeId;
double rated_power = 4800.0;
double rated_wind_speed = 18.0;
double rated_wind_speed = 14.0;
double power_step = 5.0;
double cleaning_wind_speed_step = 0.25;
double curve_wind_speed_step = 0.5;
double wind_speed_change_threshold = 1.0;
double iqr_lower_multiplier = 1.8;
double iqr_lower_multiplier = 1.2;
double iqr_upper_multiplier = 2.0;
double minimum_generator_speed = 1.0;
double generator_speed_k = 0.9;
double rotor_radius = 78.0;
double gearbox_ratio = 162.0;
double grid_connected_speed = 0.0;
double rated_generator_speed = 0.0;
bool rated_power_provided = false;
@@ -97,6 +108,17 @@ struct SchemeInfo {
std::string id;
std::string name;
std::string description;
double scheme_one_rated_power = 4800.0;
double scheme_one_rated_wind_speed = 14.0;
double scheme_one_power_step = 5.0;
double scheme_one_cleaning_wind_speed_step = 0.25;
double scheme_one_wind_speed_change_threshold = 1.0;
double scheme_one_iqr_lower_multiplier = 1.2;
double scheme_one_iqr_upper_multiplier = 2.0;
double scheme_one_minimum_generator_speed = 1.0;
double scheme_one_generator_speed_k = 0.9;
double scheme_one_rotor_radius = 78.0;
double scheme_one_gearbox_ratio = 162.0;
double grid_connected_speed = 1030.0;
double rated_generator_speed = 1755.0;
double rated_power = 2000.0;
@@ -137,6 +159,123 @@ fs::path JobRowsPath(const std::string& job_id) {
return JobDir(job_id) / "rows.jsonl";
}
fs::path JobRawRowsPath(const std::string& job_id) {
return JobDir(job_id) / "raw_rows.jsonl";
}
fs::path JobResultPath(const std::string& job_id) {
return JobDir(job_id) / "result.json";
}
std::mutex g_task_mutex;
std::string g_active_job_id;
std::chrono::steady_clock::time_point g_active_since;
bool g_active_is_upload = false;
bool IsTaskBusyFor(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
return !g_active_job_id.empty() && g_active_job_id != job_id;
}
bool AcquireTask(const std::string& job_id, bool upload) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (!g_active_job_id.empty() && g_active_job_id != job_id) return false;
g_active_job_id = job_id;
g_active_is_upload = upload;
g_active_since = std::chrono::steady_clock::now();
return true;
}
void TouchTask(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_job_id == job_id) g_active_since = std::chrono::steady_clock::now();
}
void ReleaseTask(const std::string& job_id) {
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_job_id == job_id) {
g_active_job_id.clear();
g_active_is_upload = false;
}
}
class TaskReleaseGuard {
public:
explicit TaskReleaseGuard(std::string job_id) : job_id_(std::move(job_id)) {}
~TaskReleaseGuard() { ReleaseTask(job_id_); }
private:
std::string job_id_;
};
void ExpireIdleUploadTask() {
std::string expired;
{
std::lock_guard<std::mutex> lock(g_task_mutex);
if (g_active_is_upload && !g_active_job_id.empty() &&
std::chrono::steady_clock::now() - g_active_since > kUploadIdleTimeout) {
expired = g_active_job_id;
g_active_job_id.clear();
g_active_is_upload = false;
}
}
if (!expired.empty()) {
std::error_code ignored;
fs::remove_all(JobDir(expired), ignored);
}
}
void CleanupExpiredCompletedJobs() {
std::error_code error;
if (!fs::exists(JobsRoot(), error)) return;
const auto now = fs::file_time_type::clock::now();
for (const auto& entry : fs::directory_iterator(JobsRoot(), error)) {
if (error || !entry.is_directory()) continue;
const auto job_id = entry.path().filename().string();
if (IsTaskBusyFor(job_id) || !fs::exists(entry.path() / "result.json")) continue;
const auto modified = fs::last_write_time(entry.path(), error);
if (!error && now - modified > kCompletedJobRetention) fs::remove_all(entry.path(), error);
error.clear();
}
}
std::string ExcelColumnName(size_t index) {
std::string name;
for (size_t value = index + 1; value > 0; value = (value - 1) / 26) {
name.insert(name.begin(), static_cast<char>('A' + (value - 1) % 26));
}
return name;
}
void WriteJsonCell(lxw_worksheet* sheet, lxw_row_t row, lxw_col_t column,
const json& value, lxw_format* format = nullptr) {
if (value.is_number()) {
worksheet_write_number(sheet, row, column, value.get<double>(), format);
} else if (value.is_boolean()) {
worksheet_write_boolean(sheet, row, column, value.get<bool>(), format);
} else if (!value.is_null()) {
const auto text = value.is_string() ? value.get<std::string>() : value.dump();
worksheet_write_string(sheet, row, column, text.c_str(), format);
}
}
std::string JsonText(const json& value) {
if (value.is_string()) return value.get<std::string>();
if (value.is_number_integer()) return std::to_string(value.get<long long>());
if (value.is_number_unsigned()) return std::to_string(value.get<unsigned long long>());
if (value.is_number_float()) {
std::ostringstream output;
output << value.get<double>();
return output.str();
}
return value.is_null() ? "" : value.dump();
}
std::string FileNameForFan(const std::string& fan_id) {
std::string output;
for (const auto ch : fan_id) output += std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_';
return output.empty() ? "wind_turbine" : output;
}
fs::path SchemeConfigPath() {
return fs::path("data") / "wind_schemes.json";
}
@@ -347,10 +486,16 @@ std::string NormalizeSchemeId(const std::string& scheme_id) {
}
std::vector<SchemeInfo> DefaultSchemes() {
return {
{kDefaultSchemeId, kSchemeOneName, kSchemeOneDefaultDescription},
{kSchemeTwoId, kSchemeTwoName, kSchemeTwoDefaultDescription, 1030.0, 1755.0, 2000.0},
};
SchemeInfo scheme_one;
scheme_one.id = kDefaultSchemeId;
scheme_one.name = kSchemeOneName;
scheme_one.description = kSchemeOneDefaultDescription;
SchemeInfo scheme_two;
scheme_two.id = kSchemeTwoId;
scheme_two.name = kSchemeTwoName;
scheme_two.description = kSchemeTwoDefaultDescription;
return {scheme_one, scheme_two};
}
std::optional<SchemeInfo> FindScheme(const std::vector<SchemeInfo>& schemes,
@@ -369,7 +514,21 @@ json SchemeToJson(const SchemeInfo& scheme) {
data["id"] = scheme.id;
data["name"] = scheme.name;
data["description"] = scheme.description;
if (scheme.id == kSchemeTwoId) {
if (scheme.id == kDefaultSchemeId) {
data["parameters"] = {
{"rated_power", scheme.scheme_one_rated_power},
{"rated_wind_speed", scheme.scheme_one_rated_wind_speed},
{"power_step", scheme.scheme_one_power_step},
{"cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step},
{"wind_speed_change_threshold", scheme.scheme_one_wind_speed_change_threshold},
{"iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier},
{"iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier},
{"minimum_generator_speed", scheme.scheme_one_minimum_generator_speed},
{"generator_speed_k", scheme.scheme_one_generator_speed_k},
{"rotor_radius", scheme.scheme_one_rotor_radius},
{"gearbox_ratio", scheme.scheme_one_gearbox_ratio},
};
} else if (scheme.id == kSchemeTwoId) {
data["parameters"]["grid_connected_speed"] = scheme.grid_connected_speed;
data["parameters"]["rated_generator_speed"] = scheme.rated_generator_speed;
data["parameters"]["rated_power"] = scheme.rated_power;
@@ -401,10 +560,37 @@ std::vector<SchemeInfo> LoadSchemes() {
if (description.has_value()) {
scheme.description = description.value();
}
if (scheme.id == kSchemeTwoId &&
item.contains("parameters") &&
item["parameters"].is_object()) {
const auto& params = item["parameters"];
if (!item.contains("parameters") || !item["parameters"].is_object()) {
continue;
}
const auto& params = item["parameters"];
if (scheme.id == kDefaultSchemeId) {
const auto load_positive = [&](const std::string& field, double& target) {
if (const auto value = GetNumberField(params, field);
value.has_value() && std::isfinite(value.value()) && value.value() > 0.0) {
target = value.value();
}
};
const auto load_non_negative = [&](const std::string& field, double& target) {
if (const auto value = GetNumberField(params, field);
value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0) {
target = value.value();
}
};
load_positive("rated_power", scheme.scheme_one_rated_power);
load_positive("rated_wind_speed", scheme.scheme_one_rated_wind_speed);
load_positive("power_step", scheme.scheme_one_power_step);
load_positive("cleaning_wind_speed_step", scheme.scheme_one_cleaning_wind_speed_step);
load_non_negative("wind_speed_change_threshold",
scheme.scheme_one_wind_speed_change_threshold);
load_non_negative("iqr_lower_multiplier", scheme.scheme_one_iqr_lower_multiplier);
load_non_negative("iqr_upper_multiplier", scheme.scheme_one_iqr_upper_multiplier);
load_non_negative("minimum_generator_speed",
scheme.scheme_one_minimum_generator_speed);
load_non_negative("generator_speed_k", scheme.scheme_one_generator_speed_k);
load_positive("rotor_radius", scheme.scheme_one_rotor_radius);
load_positive("gearbox_ratio", scheme.scheme_one_gearbox_ratio);
} else if (scheme.id == kSchemeTwoId) {
if (const auto value = GetNumberField(params, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
scheme.grid_connected_speed = value.value();
@@ -624,6 +810,14 @@ CalculationOptions ParseOptions(const json& body) {
value.has_value() && value.value() >= 0.0) {
options.generator_speed_k = value.value();
}
if (const auto value = GetDoubleField(opt, "rotor_radius");
value.has_value() && value.value() > 0.0) {
options.rotor_radius = value.value();
}
if (const auto value = GetDoubleField(opt, "gearbox_ratio");
value.has_value() && value.value() > 0.0) {
options.gearbox_ratio = value.value();
}
if (const auto value = GetDoubleField(opt, "grid_connected_speed");
value.has_value() && value.value() > 0.0) {
options.grid_connected_speed = value.value();
@@ -1161,10 +1355,43 @@ void WindPowerController::SaveSchemeDescription(
return;
}
std::optional<json> scheme_one_parameters;
std::optional<double> grid_connected_speed;
std::optional<double> rated_generator_speed;
std::optional<double> rated_power;
if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
if (normalized_id == kDefaultSchemeId) {
if (!body.value().contains("parameters") || !body.value()["parameters"].is_object()) {
SendError(callback, kErrorInvalidRequest, "方案一参数格式错误");
return;
}
const auto& params = body.value()["parameters"];
const auto valid_positive = [&](const std::string& field) {
const auto value = GetDoubleField(params, field);
return value.has_value() && std::isfinite(value.value()) && value.value() > 0.0;
};
const auto valid_non_negative = [&](const std::string& field) {
const auto value = GetDoubleField(params, field);
return value.has_value() && std::isfinite(value.value()) && value.value() >= 0.0;
};
if (!valid_positive("rated_power") || !valid_positive("rated_wind_speed") ||
!valid_positive("power_step") || !valid_positive("cleaning_wind_speed_step") ||
!valid_positive("rotor_radius") || !valid_positive("gearbox_ratio") ||
!valid_non_negative("wind_speed_change_threshold") ||
!valid_non_negative("iqr_lower_multiplier") ||
!valid_non_negative("iqr_upper_multiplier") ||
!valid_non_negative("minimum_generator_speed") ||
!valid_non_negative("generator_speed_k")) {
SendError(callback, kErrorInvalidRequest, "方案一参数必须为合法数值");
return;
}
if (GetDoubleField(params, "cleaning_wind_speed_step").value() > 2.0 ||
GetDoubleField(params, "iqr_lower_multiplier").value() > 10.0 ||
GetDoubleField(params, "iqr_upper_multiplier").value() > 10.0) {
SendError(callback, kErrorInvalidRequest, "方案一参数超出允许范围");
return;
}
scheme_one_parameters = params;
} else if (normalized_id == kSchemeTwoId && body.value().contains("parameters")) {
if (!body.value()["parameters"].is_object()) {
SendError(callback, kErrorInvalidRequest, "方案参数格式错误");
return;
@@ -1186,7 +1413,26 @@ void WindPowerController::SaveSchemeDescription(
for (auto& scheme : schemes) {
if (scheme.id == normalized_id) {
scheme.description = description.value();
if (scheme.id == kSchemeTwoId) {
if (scheme.id == kDefaultSchemeId) {
const auto& params = scheme_one_parameters.value();
scheme.scheme_one_rated_power = GetDoubleField(params, "rated_power").value();
scheme.scheme_one_rated_wind_speed = GetDoubleField(params, "rated_wind_speed").value();
scheme.scheme_one_power_step = GetDoubleField(params, "power_step").value();
scheme.scheme_one_cleaning_wind_speed_step =
GetDoubleField(params, "cleaning_wind_speed_step").value();
scheme.scheme_one_wind_speed_change_threshold =
GetDoubleField(params, "wind_speed_change_threshold").value();
scheme.scheme_one_iqr_lower_multiplier =
GetDoubleField(params, "iqr_lower_multiplier").value();
scheme.scheme_one_iqr_upper_multiplier =
GetDoubleField(params, "iqr_upper_multiplier").value();
scheme.scheme_one_minimum_generator_speed =
GetDoubleField(params, "minimum_generator_speed").value();
scheme.scheme_one_generator_speed_k =
GetDoubleField(params, "generator_speed_k").value();
scheme.scheme_one_rotor_radius = GetDoubleField(params, "rotor_radius").value();
scheme.scheme_one_gearbox_ratio = GetDoubleField(params, "gearbox_ratio").value();
} else if (scheme.id == kSchemeTwoId) {
if (grid_connected_speed.has_value()) {
scheme.grid_connected_speed = grid_connected_speed.value();
}
@@ -1268,9 +1514,16 @@ void WindPowerController::StartJob(
return;
}
ExpireIdleUploadTask();
CleanupExpiredCompletedJobs();
const auto job_id = GenerateJobId();
if (!AcquireTask(job_id, true)) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
try {
fs::create_directories(JobsRoot());
const auto job_id = GenerateJobId();
fs::create_directories(JobDir(job_id));
std::ofstream meta(JobDir(job_id) / "metadata.json", std::ios::trunc);
@@ -1279,11 +1532,14 @@ void WindPowerController::StartJob(
std::ofstream rows(JobRowsPath(job_id), std::ios::trunc);
rows.close();
std::ofstream raw_rows(JobRawRowsPath(job_id), std::ios::trunc);
raw_rows.close();
json data;
data["job_id"] = job_id;
SendSuccess(callback, data);
} catch (const std::exception&) {
ReleaseTask(job_id);
SendError(callback, kErrorServer, "创建计算任务失败");
}
}
@@ -1308,9 +1564,13 @@ void WindPowerController::UploadChunk(
SendError(callback, kErrorInvalidRequest, "缺少 rows 参数");
return;
}
if (IsTaskBusyFor(job_id.value())) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
try {
std::ofstream out(JobRowsPath(job_id.value()), std::ios::app);
std::ofstream raw_out(JobRawRowsPath(job_id.value()), std::ios::app);
int accepted = 0;
for (const auto& row : (*body)["rows"]) {
if (!row.is_object()) {
@@ -1320,6 +1580,13 @@ void WindPowerController::UploadChunk(
++accepted;
}
out.close();
if (body->contains("raw_rows") && (*body)["raw_rows"].is_array()) {
for (const auto& row : (*body)["raw_rows"]) {
if (row.is_object()) raw_out << row.dump() << '\n';
}
}
raw_out.close();
TouchTask(job_id.value());
json data;
data["accepted_rows"] = accepted;
@@ -1345,6 +1612,11 @@ void WindPowerController::FinishJob(
SendError(callback, kErrorJobNotFound, "计算任务不存在");
return;
}
if (IsTaskBusyFor(job_id.value())) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
TaskReleaseGuard finish_guard(job_id.value());
const CalculationOptions options = ParseOptions(*body);
if (IsSchemeTwo(options) &&
@@ -1526,7 +1798,8 @@ void WindPowerController::FinishJob(
limit_power_count += removed;
for (auto& row : fan_rows) {
row.tip_speed_ratio = row.generator_speed * 3.14 * 162.0 * 78.0 * 30.0 /
row.tip_speed_ratio = row.generator_speed * 3.14 * options.gearbox_ratio *
options.rotor_radius * 30.0 /
row.wind_speed;
}
@@ -1700,14 +1973,182 @@ void WindPowerController::FinishJob(
data["estimated_params"] = estimated_params;
try {
fs::remove_all(JobDir(job_id.value()));
std::ofstream result_file(JobResultPath(job_id.value()), std::ios::trunc);
result_file << data.dump();
} catch (const std::exception&) {
// 任务结果已经生成,临时文件清理失败不影响本次响应。
SendError(callback, kErrorServer, "保存计算结果失败");
return;
}
SendSuccess(callback, data);
}
void WindPowerController::ExportReport(
const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id) {
if (!IsSafeJobId(job_id) || !fs::exists(JobResultPath(job_id))) {
SendError(callback, kErrorJobNotFound, "计算任务不存在或结果已过期");
return;
}
std::string error;
const auto body = ParseBody(req, error);
if (!body.has_value()) {
SendError(callback, kErrorInvalidRequest, error);
return;
}
const auto fan_id = GetStringField(*body, "fan_id");
if (!fan_id.has_value() || !body->contains("effective_rows") ||
!(*body)["effective_rows"].is_array() || !body->contains("report_rows") ||
!(*body)["report_rows"].is_array()) {
SendError(callback, kErrorInvalidRequest, "报告参数不完整");
return;
}
if (!AcquireTask(job_id, false)) {
SendError(callback, kErrorJobBusy, "服务器正在处理数据,请等待当前任务完成");
return;
}
TaskReleaseGuard report_guard(job_id);
try {
json metadata;
json result;
{ std::ifstream input(JobDir(job_id) / "metadata.json"); input >> metadata; }
{ std::ifstream input(JobResultPath(job_id)); input >> result; }
const auto headers = metadata.value("raw_headers", json::array());
const auto mapping = metadata.value("mapping", json::object());
const auto fan_header = mapping.value("fan_id", "");
const auto wind_header = mapping.value("wind_speed", "");
if (!headers.is_array() || fan_header.empty() || wind_header.empty()) {
SendError(callback, kErrorServer, "任务未保存原始数据列,无法导出完整报告");
return;
}
size_t fan_column = headers.size();
size_t wind_column = headers.size();
for (size_t i = 0; i < headers.size(); ++i) {
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
if (header == fan_header) fan_column = i;
if (header == wind_header) wind_column = i;
}
if (fan_column == headers.size() || wind_column == headers.size()) {
SendError(callback, kErrorServer, "原始数据缺少风机编号或风速列");
return;
}
const auto report_path = JobDir(job_id) / ("report_" + FileNameForFan(fan_id.value()) + ".xlsx");
lxw_workbook_options options{};
const auto temp_dir = JobDir(job_id).string();
options.constant_memory = LXW_TRUE;
options.use_zip64 = LXW_TRUE;
options.tmpdir = const_cast<char*>(temp_dir.c_str());
lxw_workbook* workbook = workbook_new_opt(report_path.string().c_str(), &options);
if (!workbook) throw std::runtime_error("无法创建 Excel 工作簿");
lxw_format* header_format = workbook_add_format(workbook);
format_set_bold(header_format);
format_set_bg_color(header_format, 0xE2E8F0);
format_set_align(header_format, LXW_ALIGN_CENTER);
lxw_format* number_format = workbook_add_format(workbook);
format_set_num_format(number_format, "0.0000");
lxw_worksheet* detail = workbook_add_worksheet(workbook, "筛选后的数据");
worksheet_freeze_panes(detail, 1, 0);
const bool include_pitch = result.value("scheme", json::object()).value("id", "") == kSchemeTwoId;
const std::vector<std::string> detail_headers = include_pitch
? std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速", "3个叶片变桨角平均值"}
: std::vector<std::string>{"风机编号", "采样时间", "平均功率", "平均转速", "平均风速"};
for (size_t i = 0; i < detail_headers.size(); ++i) {
worksheet_write_string(detail, 0, i, detail_headers[i].c_str(), header_format);
worksheet_set_column(detail, i, i, i == 1 ? 22 : 16, nullptr);
}
lxw_row_t detail_row = 1;
for (const auto& point : (*body)["effective_rows"]) {
if (!point.is_object()) continue;
worksheet_write_string(detail, detail_row, 0, point.value("fan_id", fan_id.value()).c_str(), nullptr);
worksheet_write_string(detail, detail_row, 1, point.value("time", "").c_str(), nullptr);
worksheet_write_number(detail, detail_row, 2, point.value("active_power", 0.0), number_format);
worksheet_write_number(detail, detail_row, 3, point.value("generator_speed", 0.0), number_format);
worksheet_write_number(detail, detail_row, 4, point.value("wind_speed", 0.0), number_format);
if (include_pitch) worksheet_write_number(detail, detail_row, 5, point.value("pitch_angle_average", 0.0), number_format);
++detail_row;
}
worksheet_autofilter(detail, 0, 0, std::max<lxw_row_t>(1, detail_row - 1), detail_headers.size() - 1);
lxw_worksheet* raw = workbook_add_worksheet(workbook, "筛选前的数据");
worksheet_freeze_panes(raw, 1, 0);
for (size_t i = 0; i < headers.size(); ++i) {
const auto header = headers[i].is_string() ? headers[i].get<std::string>() : "";
worksheet_write_string(raw, 0, i, header.c_str(), header_format);
worksheet_set_column(raw, i, i, 16, nullptr);
}
std::set<std::string> source_files;
lxw_row_t raw_row = 1;
std::ifstream raw_input(JobRawRowsPath(job_id));
std::string line;
while (std::getline(raw_input, line)) {
const auto source = json::parse(line, nullptr, false);
if (source.is_discarded() || !source.contains("values") || !source["values"].is_array()) continue;
const auto& values = source["values"];
if (fan_column >= values.size() || JsonText(values[fan_column]) != fan_id.value()) continue;
source_files.insert(source.value("file_name", ""));
for (size_t column = 0; column < values.size(); ++column) WriteJsonCell(raw, raw_row, column, values[column]);
++raw_row;
}
if (source_files.size() != 1) {
workbook_close(workbook);
SendError(callback, kErrorInvalidRequest, "完整报告要求当前风机对应唯一一份上传 Excel 文件");
return;
}
worksheet_autofilter(raw, 0, 0, std::max<lxw_row_t>(1, raw_row - 1), headers.size() - 1);
lxw_worksheet* curve = workbook_add_worksheet(workbook, "功率曲线计算表");
worksheet_freeze_panes(curve, 1, 0);
const std::vector<std::string> curve_headers = {"序号", "风速 (m/s)", "风频时间 (h)", "计算功率 (kW)", "保证功率 (kW)", "计算发电量 (kWh)", "理论发电量 (kWh)", "", "K值"};
for (size_t i = 0; i < curve_headers.size(); ++i) {
worksheet_write_string(curve, 0, i, curve_headers[i].c_str(), header_format);
worksheet_set_column(curve, i, i, i == 0 ? 9 : 17, nullptr);
}
const auto raw_wind = ExcelColumnName(wind_column);
lxw_row_t curve_row = 1;
for (const auto& point : (*body)["report_rows"]) {
const auto excel_row = curve_row + 1;
worksheet_write_number(curve, curve_row, 0, excel_row - 1, nullptr);
worksheet_write_number(curve, curve_row, 1, point.value("wind_speed", 0.0), number_format);
const auto frequency = "=(COUNTIFS('筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选前的数据'!" + raw_wind + ":" + raw_wind + ",\"<\"&B" + std::to_string(excel_row) + "+0.5)/COUNT('筛选前的数据'!" + raw_wind + ":" + raw_wind + "))*8760";
worksheet_write_formula(curve, curve_row, 2, frequency.c_str(), number_format);
const auto actual = "=IFERROR(AVERAGEIFS('筛选后的数据'!$C:$C,'筛选后的数据'!$E:$E,\">=\"&B" + std::to_string(excel_row) + "-0.5,'筛选后的数据'!$E:$E,\"<\"&B" + std::to_string(excel_row) + "+0.5),0)";
worksheet_write_formula(curve, curve_row, 3, actual.c_str(), number_format);
worksheet_write_number(curve, curve_row, 4, point.value("design_power", 0.0), number_format);
const auto generated = "=IFERROR(ROUND(C" + std::to_string(excel_row) + "*D" + std::to_string(excel_row) + "/1000,4),0)";
const auto theoretical = "=ROUND(C" + std::to_string(excel_row) + "*E" + std::to_string(excel_row) + "/1000,4)";
worksheet_write_formula(curve, curve_row, 5, generated.c_str(), number_format);
worksheet_write_formula(curve, curve_row, 6, theoretical.c_str(), number_format);
++curve_row;
}
const auto last_row = std::max<lxw_row_t>(2, curve_row);
const auto k_formula = "=IFERROR(SUM(F2:F" + std::to_string(last_row) + ")/SUM(G2:G" + std::to_string(last_row) + "),0)";
worksheet_write_formula(curve, 1, 8, k_formula.c_str(), number_format);
worksheet_autofilter(curve, 0, 0, std::max<lxw_row_t>(1, curve_row - 1), 6);
if (body->contains("chart_image") && (*body)["chart_image"].is_string()) {
auto image_data = (*body)["chart_image"].get<std::string>();
const auto comma = image_data.find(',');
if (comma != std::string::npos) image_data = image_data.substr(comma + 1);
const auto image_path = JobDir(job_id) / "report_chart.png";
std::ofstream image(image_path, std::ios::binary | std::ios::trunc);
image << drogon::utils::base64Decode(image_data);
image.close();
worksheet_insert_image(curve, 3, 8, image_path.string().c_str());
}
if (workbook_close(workbook) != LXW_NO_ERROR) throw std::runtime_error("写入 Excel 文件失败");
callback(HttpResponse::newFileResponse(report_path.string(),
"完整功率曲线报告_" + FileNameForFan(fan_id.value()) + ".xlsx",
CT_CUSTOM,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"));
} catch (const std::exception&) {
SendError(callback, kErrorServer, "完整报告生成失败,请稍后重试");
}
}
void WindPowerController::DeleteJob(
const HttpRequestPtr&,
std::function<void(const HttpResponsePtr&)>&& callback,
@@ -1719,6 +2160,7 @@ void WindPowerController::DeleteJob(
try {
fs::remove_all(JobDir(job_id));
ReleaseTask(job_id);
SendSuccess(callback);
} catch (const std::exception&) {
SendError(callback, kErrorServer, "清理任务失败");
@@ -14,6 +14,7 @@ public:
ADD_METHOD_TO(WindPowerController::StartJob, "/api/wind/jobs/start", Post);
ADD_METHOD_TO(WindPowerController::UploadChunk, "/api/wind/jobs/chunk", Post);
ADD_METHOD_TO(WindPowerController::FinishJob, "/api/wind/jobs/finish", Post);
ADD_METHOD_TO(WindPowerController::ExportReport, "/api/wind/jobs/{1}/report", Post);
ADD_METHOD_TO(WindPowerController::DeleteJob, "/api/wind/jobs/{1}", Delete);
ADD_METHOD_TO(WindPowerController::GetSchemes, "/api/wind/schemes", Get);
ADD_METHOD_TO(WindPowerController::SaveSchemeDescription,
@@ -38,6 +39,9 @@ public:
std::function<void(const HttpResponsePtr&)>&& callback);
void FinishJob(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback);
void ExportReport(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id);
void DeleteJob(const HttpRequestPtr& req,
std::function<void(const HttpResponsePtr&)>&& callback,
const std::string& job_id);
+3 -1
View File
@@ -93,7 +93,9 @@ fi
step "3/5 上传应用与 systemd 配置..."
ssh_cmd 'command -v rsync >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq rsync >/dev/null)'
sshpass -p "${SSH_PASSWORD}" rsync -az --delete -e "ssh ${SSH_OPTS[*]}" \
# data/、uploads/ 和 logs/ 由远端服务在运行时维护;发布代码时必须保留它们。
sshpass -p "${SSH_PASSWORD}" rsync -az --delete \
--exclude '/data/' --exclude '/uploads/' --exclude '/logs/' -e "ssh ${SSH_OPTS[*]}" \
"${LOCAL_APP_DIR}/" "${TARGET}:${REMOTE_APP_PATH}/"
sshpass -p "${SSH_PASSWORD}" scp "${SSH_OPTS[@]}" \
"${DEPLOY_STAGE}/${SERVICE_NAME}.service" \
+20 -6
View File
@@ -53,7 +53,7 @@
### POST /api/wind/jobs/start
创建风功率计算任务。前端已完成 Excel 表头解析和字段映射,后端只保存任务元数据
创建风功率计算任务。服务器全局同一时刻仅允许一个上传、计算或报告生成任务;繁忙时返回 `status: 1004` 和“服务器正在处理数据,请等待当前任务完成”
请求:
@@ -71,7 +71,8 @@
"wind_speed": "风速(m/s)",
"active_power": "有功功率(kW)",
"generator_speed": "发电机转速(rpm)"
}
},
"raw_headers": ["时间", "风机编号", "风速(m/s)", "有功功率(kW)", "发电机转速(rpm)"]
}
```
@@ -89,7 +90,7 @@
### POST /api/wind/jobs/chunk
上传标准化后的数据分片。每行字段必须使用标准字段名
上传标准化后的数据分片。`raw_rows` 保存原始 Excel 行,用于服务端完整报告的“筛选前的数据”工作表
请求:
@@ -105,6 +106,9 @@
"active_power": 114.3471,
"generator_speed": 1109.3143
}
],
"raw_rows": [
{"file_name": "01_风机历史数据.xls", "values": ["2024-09-01 14:00:00", "01#", 3.414, 114.3471, 1109.3143]}
]
}
```
@@ -131,14 +135,18 @@
{
"job_id": "job_123",
"options": {
"rated_power": 4800,
"rated_wind_speed": 14,
"power_step": 5,
"cleaning_wind_speed_step": 0.25,
"curve_wind_speed_step": 0.5,
"wind_speed_change_threshold": 1,
"iqr_lower_multiplier": 1.8,
"iqr_lower_multiplier": 1.2,
"iqr_upper_multiplier": 2,
"minimum_generator_speed": 1,
"generator_speed_k": 0.9
"generator_speed_k": 0.9,
"rotor_radius": 78,
"gearbox_ratio": 162
}
}
```
@@ -229,7 +237,7 @@
- `wind_speed <= 0` 的数据剔除。
- 同一风机同一时间重复记录保留第一条。
- 限功率识别按参考脚本执行:按功率分箱、按日期分组,组内风速跨度大于阈值时剔除。
- 叶尖速比使用参考脚本公式 `generator_speed * 3.14 * 162 * 78 * 30 / wind_speed`按风速分箱做 IQR 清洗。
- 叶尖速比 `generator_speed * 3.14 * gearbox_ratio * rotor_radius * 30 / wind_speed` 计算;`3.14``30` 固定,叶轮半径与齿轮箱传动比由方案一参数提供,再按风速分箱做 IQR 清洗。
- 风速-功率关系按同一风速分箱和 IQR 参数清洗。
- 每台风机在 IQR 清洗后自动估算平台功率和平台起始风速,返回到 `estimated_params`
- 高风速平台区明显低于平台功率的点剔除为 `high_wind_low_power`
@@ -239,6 +247,12 @@
- `scatter_points` 返回清洗后保留点,`filtered_points` 返回所有过滤阶段滤除的点和原因。
- `estimated_params.source``auto``auto_power_fallback_wind``fallback`
### POST /api/wind/jobs/{jobId}/report
服务端以常量内存方式生成完整报告并直接返回 `.xlsx` 附件。计算完成的数据保留 24 小时;上传任务连续 30 分钟未活动会自动清理。
请求体包含 `fan_id`、当前编辑后的 `effective_rows``report_rows` 和可选的 `chart_image`PNG data URL)。成功时响应为 Excel 文件流;失败时仍使用统一 JSON 响应信封。
### DELETE /api/wind/jobs/{job_id}
清理未完成任务的临时文件。
+45
View File
@@ -254,6 +254,13 @@
margin-top: 14px;
}
.schemeParamHint {
margin: 14px 0 0;
color: #64748b;
font-size: 13px;
line-height: 1.5;
}
.schemeParamGrid input {
min-width: 0;
width: 100%;
@@ -805,6 +812,44 @@ td {
.error { border-color: #fecaca; background: #fff1f2; color: #b91c1c; }
.alert.info { border-color: #bfdbfe; background: #eff6ff; color: #1d4ed8; }
.dialogBackdrop {
position: fixed;
z-index: 1000;
inset: 0;
display: grid;
place-items: center;
padding: 20px;
background: rgba(15, 23, 42, 0.42);
}
.appDialog {
width: min(440px, 100%);
padding: 22px;
border: 1px solid #dbe3ed;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 20px 48px rgba(15, 23, 42, 0.22);
}
.appDialog h2 {
margin: 0;
color: #172033;
font-size: 18px;
}
.appDialog p {
margin: 12px 0 0;
color: #475569;
font-size: 14px;
line-height: 1.6;
}
.appDialogActions {
display: flex;
justify-content: flex-end;
margin-top: 20px;
}
.schemeDescriptionControl span,
.fieldControl span { color: #334155; }
.schemeDescriptionControl input,
+294 -99
View File
@@ -4,6 +4,7 @@ import 'uplot/dist/uPlot.min.css';
import * as XLSX from 'xlsx';
import {
deleteWindJob,
downloadWindReport,
finishWindJob,
getWindChartOptions,
getWindSchemes,
@@ -58,7 +59,24 @@ const EDIT_TOOL_ERASE = 'erase';
const DEFAULT_SCHEME_ID = 'scheme_one';
const SCHEME_TWO_ID = 'scheme_two';
const DEFAULT_SCHEMES = [
{ id: 'scheme_one', name: '方案一', description: '通用方案' },
{
id: 'scheme_one',
name: '方案一',
description: '通用方案',
parameters: {
rated_power: 4800,
rated_wind_speed: 14,
power_step: 5,
cleaning_wind_speed_step: 0.25,
wind_speed_change_threshold: 1,
iqr_lower_multiplier: 1.2,
iqr_upper_multiplier: 2,
minimum_generator_speed: 1,
generator_speed_k: 0.9,
rotor_radius: 78,
gearbox_ratio: 162,
},
},
{
id: SCHEME_TWO_ID,
name: '方案二',
@@ -76,6 +94,32 @@ const DEFAULT_SCHEME_TWO_PARAMS = {
rated_generator_speed: '1755',
rated_power: '2000',
};
const DEFAULT_SCHEME_ONE_PARAMS = {
rated_power: '4800',
rated_wind_speed: '14',
power_step: '5',
cleaning_wind_speed_step: '0.25',
wind_speed_change_threshold: '1',
iqr_lower_multiplier: '1.2',
iqr_upper_multiplier: '2',
minimum_generator_speed: '1',
generator_speed_k: '0.9',
rotor_radius: '78',
gearbox_ratio: '162',
};
const SCHEME_ONE_PARAM_FIELDS = [
{ key: 'rated_power', label: '额定功率', unit: 'kW', min: '0', step: '0.1' },
{ key: 'rated_wind_speed', label: '额定风速', unit: 'm/s', min: '0', step: '0.01' },
{ key: 'power_step', label: '功率分箱步长', unit: 'kW', min: '0', step: '0.01' },
{ key: 'cleaning_wind_speed_step', label: '清洗风速分箱步长', unit: 'm/s', min: '0', step: '0.01' },
{ key: 'wind_speed_change_threshold', label: '风速变化阈值', unit: 'm/s', min: '0', step: '0.01' },
{ key: 'iqr_lower_multiplier', label: 'IQR 下限系数', unit: '', min: '0', step: '0.01' },
{ key: 'iqr_upper_multiplier', label: 'IQR 上限系数', unit: '', min: '0', step: '0.01' },
{ key: 'minimum_generator_speed', label: '最小发电机转速', unit: 'rpm', min: '0', step: '0.01' },
{ key: 'generator_speed_k', label: '发电机转速系数 K', unit: '', min: '0', step: '0.01' },
{ key: 'rotor_radius', label: '叶轮半径', unit: 'm', min: '0', step: '0.01' },
{ key: 'gearbox_ratio', label: '齿轮箱传动比', unit: '', min: '0', step: '0.01' },
];
const CHART_STATE_KEY = 'wind_power_chart_state_v1';
const DEFAULT_CHART_OPTIONS = {
title: '#机组功率曲线',
@@ -393,11 +437,13 @@ function buildStandardRows(files, mapping) {
);
const rows = [];
const rawRows = [];
for (const file of files) {
const localIndexes = Object.fromEntries(
REQUIRED_FIELDS.map((field) => [field.key, file.headers.indexOf(mapping[field.key])]),
);
for (const row of file.rows) {
for (let rowIndex = 0; rowIndex < file.rows.length; rowIndex += 1) {
const row = file.rows[rowIndex];
rows.push({
time: normalizeTime(getCell(row, localIndexes.time)),
fan_id: String(getCell(row, localIndexes.fan_id) ?? '').trim(),
@@ -408,10 +454,14 @@ function buildStandardRows(files, mapping) {
blade_pitch_2: normalizeNumber(getCell(row, localIndexes.blade_pitch_2)),
blade_pitch_3: normalizeNumber(getCell(row, localIndexes.blade_pitch_3)),
});
rawRows.push({
file_name: file.file_name,
values: [...(file.raw_rows?.[rowIndex] || row)],
});
}
}
return { rows, indexes };
return { rows, rawRows, indexes };
}
function hasMatchingHeaderSequence(files) {
@@ -423,44 +473,6 @@ function hasMatchingHeaderSequence(files) {
));
}
function findRawSourceFiles(files, mapping, fanId) {
if (!fanId) return [];
return files.filter((file) => {
const fanColumnIndex = file.headers.indexOf(mapping.fan_id);
return fanColumnIndex >= 0 && file.rows.some((row) => (
String(row[fanColumnIndex] ?? '').trim() === fanId
));
});
}
function buildRawReportData(file, mapping, fanId) {
if (!file || !fanId) return null;
const fanColumnIndex = file.headers.indexOf(mapping.fan_id);
const windSpeedColumnIndex = file.headers.indexOf(mapping.wind_speed);
if (fanColumnIndex < 0 || windSpeedColumnIndex < 0) return null;
const rows = file.rows.reduce((result, row, index) => {
if (String(row[fanColumnIndex] ?? '').trim() === fanId) {
const rawRow = [...(file.raw_rows?.[index] || row)];
const windSpeed = normalizeNumber(rawRow[windSpeedColumnIndex]);
// 部分源 Excel 将风速保存为“数字文本”。保留显示值的同时写成数值单元格,
// 使计算表的 COUNTIFS 能正确统计风频。
if (Number.isFinite(windSpeed)) {
rawRow[windSpeedColumnIndex] = windSpeed;
}
result.push(rawRow);
}
return result;
}, []);
return {
headers: [...(file.raw_headers || file.headers)],
rows,
windSpeedColumnIndex,
};
}
function formatTimestampForFile(date = new Date()) {
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}`
+ `${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
@@ -585,6 +597,13 @@ function schemeParamsToState(parameters) {
};
}
function schemeOneParamsToState(parameters) {
return Object.fromEntries(Object.entries(DEFAULT_SCHEME_ONE_PARAMS).map(([field, fallback]) => [
field,
String(parameters?.[field] ?? fallback),
]));
}
function SummaryCards({ summary }) {
if (!summary) return null;
const cards = [
@@ -972,14 +991,25 @@ async function captureChartFrame(frame) {
};
}
function downloadReportBlob(blob, fanId) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `完整功率曲线报告_${sanitizeFileName(fanId)}_${formatTimestampForFile()}.xlsx`;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 0);
}
/* 已迁移到 reportExportWorker,由 Worker 承担 ExcelJS 的内存压力。 */
/*
const REPORT_DETAIL_HEADERS = [
'风机编号',
'采样时间',
'平均功率',
'平均转速',
'平均风速',
'3个叶片变桨角平均值',
];
const REPORT_PITCH_HEADER = '3个叶片变桨角平均值';
const REPORT_BORDER = {
top: { style: 'thin', color: { argb: 'FF475569' } },
@@ -1000,16 +1030,19 @@ function reportPitchAverage(row) {
return values.every((value) => value !== '') ? values.reduce((sum, value) => sum + value, 0) / values.length : '';
}
function reportDetailValues(row, fanId) {
function reportDetailValues(row, fanId, includePitch) {
const savedPitchAverage = reportNumber(row.pitch_angle_average);
return [
const values = [
row.fan_id || fanId || '',
row.time || '',
reportNumber(row.active_power),
reportNumber(row.generator_speed),
reportNumber(row.wind_speed),
savedPitchAverage !== '' ? savedPitchAverage : reportPitchAverage(row),
];
if (includePitch) {
values.push(savedPitchAverage !== '' ? savedPitchAverage : reportPitchAverage(row));
}
return values;
}
function styleReportHeader(row) {
@@ -1036,17 +1069,25 @@ function styleReportBody(sheet, startRow, endRow, numericColumns) {
}
function addReportDetailSheet(workbook, name, rows, fanId) {
const includePitch = rows.some((row) => (
reportNumber(row.pitch_angle_average) !== '' || reportPitchAverage(row) !== ''
));
const headers = includePitch ? [...REPORT_DETAIL_HEADERS, REPORT_PITCH_HEADER] : REPORT_DETAIL_HEADERS;
const sheet = workbook.addWorksheet(name, {
views: [{ state: 'frozen', ySplit: 1 }],
});
sheet.columns = [
{ width: 16 }, { width: 22 }, { width: 14 }, { width: 14 }, { width: 14 }, { width: 23 },
{ width: 16 }, { width: 22 }, { width: 14 }, { width: 14 }, { width: 14 },
...(includePitch ? [{ width: 23 }] : []),
];
sheet.addRow(REPORT_DETAIL_HEADERS);
rows.forEach((row) => sheet.addRow(reportDetailValues(row, fanId)));
sheet.addRow(headers);
rows.forEach((row) => sheet.addRow(reportDetailValues(row, fanId, includePitch)));
styleReportHeader(sheet.getRow(1));
styleReportBody(sheet, 2, Math.max(2, rows.length + 1), [3, 4, 5, 6]);
sheet.autoFilter = `A1:F${Math.max(1, rows.length + 1)}`;
// 对每个数据单元格设置样式会使 ExcelJS 为大量行保留大量对象,容易耗尽浏览器内存。
for (const columnNumber of includePitch ? [3, 4, 5, 6] : [3, 4, 5]) {
sheet.getColumn(columnNumber).numFmt = '0.0000';
}
sheet.autoFilter = `A1:${includePitch ? 'F' : 'E'}${Math.max(1, rows.length + 1)}`;
return sheet;
}
@@ -1067,14 +1108,15 @@ function addRawReportSheet(workbook, { headers, rows }) {
});
sheet.addRow(headers);
rows.forEach((row) => sheet.addRow(row));
// 列宽只需参考样本即可,避免大文件导出时重复扫描全部单元格。
const widthSampleRows = rows.slice(0, 1000);
sheet.columns = headers.map((header, index) => {
const maxLength = rows.reduce((max, row) => (
const maxLength = widthSampleRows.reduce((max, row) => (
Math.max(max, String(row[index] ?? '').length)
), String(header ?? '').length);
return { width: Math.min(32, Math.max(12, maxLength + 2)) };
});
styleReportHeader(sheet.getRow(1));
styleReportBody(sheet, 2, Math.max(2, rows.length + 1), []);
sheet.autoFilter = `A1:${excelColumnName(Math.max(0, headers.length - 1))}${Math.max(1, rows.length + 1)}`;
return sheet;
}
@@ -1086,7 +1128,6 @@ async function exportPowerCurveReport({
reportRows,
chartRenderSnapshot,
}) {
const { default: ExcelJS } = await import('exceljs');
const workbook = new ExcelJS.Workbook();
workbook.creator = '风电功率计算平台';
workbook.created = new Date();
@@ -1157,6 +1198,8 @@ async function exportPowerCurveReport({
URL.revokeObjectURL(url);
}
*/
function PowerCurveChart({
points,
scatterPoints,
@@ -1218,7 +1261,7 @@ function PowerCurveChart({
const yAxisSize = Math.max(64, Math.round(screenTypography.tick * 3.2 + 16));
const axisLabelSize = screenTypography.axis + 18;
const showScatterOnChart = activeChartOptions.show_scatter || editMode;
chartSnapshotRef.current = async () => {
chartSnapshotRef.current = async (captureFrame = true) => {
const snapshot = {
actualCurve: visibleActualCurve,
designCurve: visibleDesignCurve,
@@ -1231,6 +1274,9 @@ function PowerCurveChart({
options: activeChartOptions,
scales: plotRef.current?.scales,
};
if (!captureFrame) {
return snapshot;
}
try {
const capturedImage = await captureChartFrame(frameRef.current);
return capturedImage ? { ...snapshot, ...capturedImage } : snapshot;
@@ -1244,6 +1290,8 @@ function PowerCurveChart({
useEffect(() => {
if (!chartRef.current || (!visibleActualCurve.length && !visibleDesignCurve.length)) return undefined;
const transientErasedPointIds = new Set();
let redrawFrameId = 0;
const sortedCurve = [...visibleActualCurve].sort((left, right) => left.wind_speed - right.wind_speed);
const sortedDesign = [...visibleDesignCurve].sort((left, right) => left.wind_speed - right.wind_speed);
const actualByWindSpeed = new Map(
@@ -1372,6 +1420,9 @@ function PowerCurveChart({
if (showScatterOnChart) {
ctx.fillStyle = hexToRgba(activeChartOptions.scatter_color, activeChartOptions.scatter_opacity);
for (const point of validScatter) {
if (transientErasedPointIds.has(point.point_id)) {
continue;
}
const x = u.valToPos(point.wind_speed, 'x', true);
const y = u.valToPos(point.active_power, 'y', true);
if (Number.isFinite(x) && Number.isFinite(y)) {
@@ -1437,6 +1488,16 @@ function PowerCurveChart({
plotRef.current = chart;
setSeriesVisibility({ actual: true, design: true });
const requestChartRedraw = () => {
if (redrawFrameId) {
return;
}
redrawFrameId = window.requestAnimationFrame(() => {
redrawFrameId = 0;
chart.redraw(true, false);
});
};
const hideBrush = () => {
setBrush((prev) => ({ ...prev, visible: false, dragging: false }));
};
@@ -1449,6 +1510,7 @@ function PowerCurveChart({
}
let dragging = false;
const pendingPoints = new Map();
const updateBrush = (event, nextDragging = dragging) => {
if (!frameRef.current) return;
const rect = frameRef.current.getBoundingClientRect();
@@ -1484,16 +1546,32 @@ function PowerCurveChart({
if (!hitPoints.length) {
return;
}
for (const point of hitPoints) {
const pointId = point.point_id || `${point.wind_speed}_${point.active_power}`;
pendingPoints.set(pointId, point);
if (editTool === EDIT_TOOL_ERASE) {
transientErasedPointIds.add(pointId);
}
}
if (editTool === EDIT_TOOL_ERASE) {
onErasePoints?.(hitPoints);
requestChartRedraw();
}
};
const applyPendingPoints = () => {
if (!pendingPoints.size) return;
const points = Array.from(pendingPoints.values());
pendingPoints.clear();
if (editTool === EDIT_TOOL_ERASE) {
onErasePoints?.(points);
} else {
onRestorePoints?.(hitPoints);
onRestorePoints?.(points);
}
};
const handlePointerDown = (event) => {
if (event.button !== 0) return;
event.preventDefault();
dragging = true;
chart.over.setPointerCapture?.(event.pointerId);
updateBrush(event, true);
applyToolAt(event);
};
@@ -1506,6 +1584,11 @@ function PowerCurveChart({
const handlePointerUp = (event) => {
updateBrush(event, false);
dragging = false;
applyToolAt(event);
applyPendingPoints();
if (chart.over.hasPointerCapture?.(event.pointerId)) {
chart.over.releasePointerCapture(event.pointerId);
}
};
chart.over.addEventListener('pointerdown', handlePointerDown);
@@ -1518,6 +1601,10 @@ function PowerCurveChart({
chart.over.removeEventListener('pointermove', handlePointerMove);
chart.over.removeEventListener('pointerleave', hideBrush);
window.removeEventListener('pointerup', handlePointerUp);
if (redrawFrameId) {
window.cancelAnimationFrame(redrawFrameId);
}
pendingPoints.clear();
if (plotRef.current === chart) plotRef.current = null;
chart.destroy();
};
@@ -1626,6 +1713,8 @@ export default function HomePage() {
const [savingChartOptions, setSavingChartOptions] = useState(false);
const [chartSettingsMessage, setChartSettingsMessage] = useState('');
const [exportingReport, setExportingReport] = useState(false);
const [reportExportProgress, setReportExportProgress] = useState('');
const [reportExportNotice, setReportExportNotice] = useState('');
const chartExportRef = useRef(() => false);
const chartSnapshotRef = useRef(() => null);
const [schemes, setSchemes] = useState(DEFAULT_SCHEMES);
@@ -1633,12 +1722,14 @@ export default function HomePage() {
const [schemeDescription, setSchemeDescription] = useState(DEFAULT_SCHEMES[0].description);
const [savingScheme, setSavingScheme] = useState(false);
const [schemeMessage, setSchemeMessage] = useState('');
const [schemeOneParams, setSchemeOneParams] = useState(DEFAULT_SCHEME_ONE_PARAMS);
const [schemeTwoParams, setSchemeTwoParams] = useState(DEFAULT_SCHEME_TWO_PARAMS);
const headers = files[0]?.headers || [];
const missingFields = REQUIRED_FIELDS.filter((field) => !mapping[field.key]);
const selectedScheme = schemes.find((scheme) => scheme.id === selectedSchemeId) || schemes[0];
const isSchemeTwo = selectedSchemeId === SCHEME_TWO_ID;
const mappingFields = isSchemeTwo ? REQUIRED_FIELDS : REQUIRED_FIELDS.slice(0, 5);
const missingFields = mappingFields.filter((field) => !mapping[field.key]);
const selectedPoints = selectedFan && result?.curves ? result.curves[selectedFan] || [] : [];
const selectedBins = selectedFan && result?.bins ? result.bins[selectedFan] || [] : [];
const selectedScatterRaw = selectedFan && result?.scatter_points
@@ -1702,14 +1793,6 @@ export default function HomePage() {
() => hasMatchingHeaderSequence(files),
[files],
);
const rawReportSourceFiles = useMemo(
() => findRawSourceFiles(files, mapping, selectedFan),
[files, mapping, selectedFan],
);
const selectedRawReportData = useMemo(() => {
if (!hasConsistentRawHeaders || rawReportSourceFiles.length !== 1) return null;
return buildRawReportData(rawReportSourceFiles[0], mapping, selectedFan);
}, [hasConsistentRawHeaders, mapping, rawReportSourceFiles, selectedFan]);
const selectedEstimatedParams = selectedFan && result?.estimated_params
? result.estimated_params[selectedFan]
: null;
@@ -1924,48 +2007,68 @@ export default function HomePage() {
() => buildReportCurveRows(effectiveScatter, reportDesignCurve),
[effectiveScatter, reportDesignCurve],
);
const reportExportDisabledReason = files.length && !hasConsistentRawHeaders
? '同一批上传文件的列头和列顺序必须完全一致'
: files.length && rawReportSourceFiles.length !== 1
? '完整报告要求当前风机对应唯一一份上传 Excel 文件'
: files.length && (!selectedRawReportData || !selectedRawReportData.rows.length)
? '当前风机没有可导出的原始数据,或未识别到风速列'
const reportExportDisabledReason = !selectedFan
? '请先完成计算并选择要导出的风机'
: isEditMode
? '请先退出曲线编辑模式,再导出完整报告'
: !reportDesignCurve.length
? '请先填写或上传设计功率曲线'
: !result?.job_id
? '当前计算结果已失效,请重新计算后导出报告'
: files.length && !hasConsistentRawHeaders
? '同一批上传文件的列头和列顺序必须完全一致'
: !reportCurveRows.length
? '当前设计功率曲线与有效数据未形成可导出的功率曲线点'
: '';
const canExportFullReport = Boolean(
selectedFan && selectedRawReportData?.rows.length && reportCurveRows.length && !isEditMode &&
!reportExportDisabledReason,
);
const handleExportFullReport = useCallback(async () => {
if (!canExportFullReport || exportingReport) return;
if (exportingReport) return;
if (!canExportFullReport) {
setReportExportNotice(reportExportDisabledReason || '当前条件不满足,无法导出完整报告');
return;
}
setExportingReport(true);
setReportExportProgress('正在服务器生成完整报告');
setError('');
try {
const chartRenderSnapshot = await chartSnapshotRef.current?.();
const chartRenderSnapshot = await chartSnapshotRef.current?.(false);
if (!chartRenderSnapshot) {
throw new Error('图表尚未完成绘制,请稍后重试');
}
await exportPowerCurveReport({
fanId: selectedFan,
rawReportData: selectedRawReportData,
effectiveRows: effectiveScatter,
reportRows: reportCurveRows,
chartRenderSnapshot,
const chartImageData = renderPowerCurvePng(chartRenderSnapshot);
const blob = await downloadWindReport(result.job_id, {
fan_id: selectedFan,
effective_rows: effectiveScatter,
report_rows: reportCurveRows,
chart_image: chartImageData || '',
});
downloadReportBlob(blob, selectedFan);
} catch (err) {
setError(err.message || '导出完整报告失败');
} finally {
const message = err.message || '导出完整报告失败';
setError(message);
setReportExportNotice(message);
setExportingReport(false);
setReportExportProgress('');
}
}, [
canExportFullReport,
effectiveScatter,
exportingReport,
reportExportDisabledReason,
reportCurveRows,
rawReportSourceFiles,
selectedRawReportData,
selectedFan,
result?.job_id,
]);
const handleCancelReportExport = useCallback(() => {
setExportingReport(false);
setReportExportProgress('');
setReportExportNotice('已取消完整报告导出');
}, []);
useEffect(() => {
let canceled = false;
@@ -1981,6 +2084,8 @@ export default function HomePage() {
setSchemes(loadedSchemes);
setSelectedSchemeId(defaultScheme.id);
setSchemeDescription(defaultScheme.description || '');
const schemeOne = loadedSchemes.find((scheme) => scheme.id === DEFAULT_SCHEME_ID);
setSchemeOneParams(schemeOneParamsToState(schemeOne?.parameters));
const schemeTwo = loadedSchemes.find((scheme) => scheme.id === SCHEME_TWO_ID);
setSchemeTwoParams(schemeParamsToState(schemeTwo?.parameters));
} catch (err) {
@@ -2000,7 +2105,9 @@ export default function HomePage() {
const nextScheme = schemes.find((scheme) => scheme.id === value);
setSelectedSchemeId(value);
setSchemeDescription(nextScheme?.description || '');
if (value === SCHEME_TWO_ID) {
if (value === DEFAULT_SCHEME_ID) {
setSchemeOneParams(schemeOneParamsToState(nextScheme?.parameters));
} else if (value === SCHEME_TWO_ID) {
setSchemeTwoParams(schemeParamsToState(nextScheme?.parameters));
}
setSchemeMessage('');
@@ -2013,6 +2120,13 @@ export default function HomePage() {
}));
}
function handleSchemeOneParamChange(field, value) {
setSchemeOneParams((prev) => ({
...prev,
[field]: value,
}));
}
async function handleSaveSchemeDescription() {
if (!selectedScheme) return;
@@ -2020,9 +2134,26 @@ export default function HomePage() {
setError('');
setSchemeMessage('');
try {
const schemeOneValues = Object.fromEntries(Object.entries(schemeOneParams).map(([field, value]) => [
field,
normalizeNumber(value),
]));
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
if (selectedScheme.id === DEFAULT_SCHEME_ID &&
(!Number.isFinite(schemeOneValues.rated_power) || schemeOneValues.rated_power <= 0 ||
!Number.isFinite(schemeOneValues.rated_wind_speed) || schemeOneValues.rated_wind_speed <= 0 ||
!Number.isFinite(schemeOneValues.power_step) || schemeOneValues.power_step <= 0 ||
!Number.isFinite(schemeOneValues.cleaning_wind_speed_step) || schemeOneValues.cleaning_wind_speed_step <= 0 ||
!Number.isFinite(schemeOneValues.rotor_radius) || schemeOneValues.rotor_radius <= 0 ||
!Number.isFinite(schemeOneValues.gearbox_ratio) || schemeOneValues.gearbox_ratio <= 0 ||
['wind_speed_change_threshold', 'iqr_lower_multiplier', 'iqr_upper_multiplier',
'minimum_generator_speed', 'generator_speed_k'].some((field) => (
!Number.isFinite(schemeOneValues[field]) || schemeOneValues[field] < 0
)))) {
throw new Error('方案一参数必须为合法数值,额定与步长参数、叶轮半径和传动比必须大于 0');
}
if (selectedScheme.id === SCHEME_TWO_ID &&
(!Number.isFinite(gridConnectedSpeed) || gridConnectedSpeed <= 0 ||
!Number.isFinite(ratedGeneratorSpeed) || ratedGeneratorSpeed <= 0 ||
@@ -2031,6 +2162,7 @@ export default function HomePage() {
}
const data = await saveWindSchemeDescription(selectedScheme.id, {
description: schemeDescription,
...(selectedScheme.id === DEFAULT_SCHEME_ID ? { parameters: schemeOneValues } : {}),
...(selectedScheme.id === SCHEME_TWO_ID ? {
parameters: {
grid_connected_speed: gridConnectedSpeed,
@@ -2044,7 +2176,9 @@ export default function HomePage() {
scheme.id === savedScheme.id ? { ...scheme, ...savedScheme } : scheme
)));
setSchemeDescription(savedScheme.description || '');
if (savedScheme.id === SCHEME_TWO_ID) {
if (savedScheme.id === DEFAULT_SCHEME_ID) {
setSchemeOneParams(schemeOneParamsToState(savedScheme.parameters));
} else if (savedScheme.id === SCHEME_TWO_ID) {
setSchemeTwoParams(schemeParamsToState(savedScheme.parameters));
}
setSchemeMessage('已保存');
@@ -2142,6 +2276,26 @@ export default function HomePage() {
setError('请先上传文件并完成所有字段映射');
return;
}
const schemeOneValues = Object.fromEntries(Object.entries(schemeOneParams).map(([field, value]) => [
field,
normalizeNumber(value),
]));
const schemeOneInvalid =
!Number.isFinite(schemeOneValues.rated_power) || schemeOneValues.rated_power <= 0 ||
!Number.isFinite(schemeOneValues.rated_wind_speed) || schemeOneValues.rated_wind_speed <= 0 ||
!Number.isFinite(schemeOneValues.power_step) || schemeOneValues.power_step <= 0 ||
!Number.isFinite(schemeOneValues.cleaning_wind_speed_step) ||
schemeOneValues.cleaning_wind_speed_step <= 0 ||
!Number.isFinite(schemeOneValues.rotor_radius) || schemeOneValues.rotor_radius <= 0 ||
!Number.isFinite(schemeOneValues.gearbox_ratio) || schemeOneValues.gearbox_ratio <= 0 ||
['wind_speed_change_threshold', 'iqr_lower_multiplier', 'iqr_upper_multiplier',
'minimum_generator_speed', 'generator_speed_k'].some((field) => (
!Number.isFinite(schemeOneValues[field]) || schemeOneValues[field] < 0
));
if (!isSchemeTwo && schemeOneInvalid) {
setError('方案一参数必须为合法数值,额定与步长参数、叶轮半径和传动比必须大于 0');
return;
}
const gridConnectedSpeed = normalizeNumber(schemeTwoParams.grid_connected_speed);
const ratedGeneratorSpeed = normalizeNumber(schemeTwoParams.rated_generator_speed);
const ratedPower = normalizeNumber(schemeTwoParams.rated_power);
@@ -2166,13 +2320,14 @@ export default function HomePage() {
setProgress('正在准备标准化数据');
try {
const { rows } = buildStandardRows(files, mapping);
const { rows, rawRows } = buildStandardRows(files, mapping);
const start = await startWindJob({
files: files.map((file) => ({
file_name: file.file_name,
row_count: file.row_count,
})),
mapping,
raw_headers: files[0]?.raw_headers || files[0]?.headers || [],
});
jobId = start.job_id;
@@ -2184,6 +2339,7 @@ export default function HomePage() {
job_id: jobId,
chunk_index: index,
rows: chunkRows,
raw_rows: rawRows.slice(index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE),
});
}
@@ -2191,15 +2347,9 @@ export default function HomePage() {
const calculation = await finishWindJob({
job_id: jobId,
options: {
power_step: 5,
cleaning_wind_speed_step: 0.25,
curve_wind_speed_step: 0.5,
wind_speed_change_threshold: 1,
iqr_lower_multiplier: 1.8,
iqr_upper_multiplier: 2,
minimum_generator_speed: 1,
generator_speed_k: 0.9,
scheme_id: selectedSchemeId,
...(!isSchemeTwo ? schemeOneValues : {}),
...(isSchemeTwo ? {
grid_connected_speed: gridConnectedSpeed,
rated_generator_speed: ratedGeneratorSpeed,
@@ -2207,7 +2357,7 @@ export default function HomePage() {
} : {}),
},
});
setResult(calculation);
setResult({ ...calculation, job_id: jobId });
setSelectedFan(calculation.fans?.[0] || '');
setProgress('计算完成');
} catch (err) {
@@ -2218,7 +2368,11 @@ export default function HomePage() {
// 失败任务清理失败不影响用户看到主错误。
}
}
setError(err.message || '计算失败');
const message = err.message || '计算失败';
setError(message);
if (message.includes('服务器正在处理数据')) {
setReportExportNotice(message);
}
setProgress('');
} finally {
setSubmitting(false);
@@ -2248,6 +2402,19 @@ export default function HomePage() {
{error && <div className="alert error">{error}</div>}
{progress && <div className="alert info">{progress}</div>}
{reportExportNotice && (
<div className="dialogBackdrop" role="presentation">
<section className="appDialog" role="dialog" aria-modal="true" aria-labelledby="report-export-notice-title">
<h2 id="report-export-notice-title">提示</h2>
<p>{reportExportNotice}</p>
<div className="appDialogActions">
<button className="primaryButton" type="button" onClick={() => setReportExportNotice('')}>
我知道了
</button>
</div>
</section>
</div>
)}
<section className="panel schemePanel">
<div className="panelHeader">
@@ -2283,6 +2450,29 @@ export default function HomePage() {
/>
</label>
</div>
{!isSchemeTwo && (
<>
<p className="schemeParamHint">
叶尖速比 = 发电机转速 × 3.14 × 齿轮箱传动比 × 叶轮半径 × 30 ÷ 风速
</p>
<div className="schemeParamGrid">
{SCHEME_ONE_PARAM_FIELDS.map((field) => (
<label className="fieldControl" key={field.key}>
<span>{field.label}{field.unit ? ` (${field.unit})` : ''}</span>
<input
type="number"
min={field.min}
step={field.step}
value={schemeOneParams[field.key]}
onChange={(event) => handleSchemeOneParamChange(field.key, event.target.value)}
disabled={submitting || savingScheme}
placeholder="请输入"
/>
</label>
))}
</div>
</>
)}
{isSchemeTwo && (
<div className="schemeParamGrid">
<label className="fieldControl">
@@ -2457,7 +2647,7 @@ export default function HomePage() {
<div className="emptyState">上传文件后可指定列头对应关系</div>
) : (
<div className="mappingGrid">
{REQUIRED_FIELDS.map((field) => (
{mappingFields.map((field) => (
<label className="fieldControl" key={field.key}>
<span>{field.label}</span>
<select
@@ -2594,11 +2784,16 @@ export default function HomePage() {
className="secondaryButton"
type="button"
onClick={handleExportFullReport}
disabled={!canExportFullReport || exportingReport}
disabled={exportingReport}
title={reportExportDisabledReason || '导出当前风机的完整功率曲线报告'}
>
{exportingReport ? '导出报告中...' : '导出完整报告'}
{exportingReport ? (reportExportProgress || '导出报告中...') : '导出完整报告'}
</button>
{exportingReport && (
<button className="secondaryButton" type="button" onClick={handleCancelReportExport}>
取消导出
</button>
)}
<button
className="secondaryButton"
type="button"
+21
View File
@@ -141,3 +141,24 @@ export function deleteWindJob(jobId) {
method: 'DELETE',
});
}
/** 服务端生成完整报告并以文件流返回。 */
export async function downloadWindReport(jobId, payload) {
const resp = await fetch(`${BASE_URL}/wind/jobs/${encodeURIComponent(jobId)}/report`, {
method: 'POST',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) {
const text = await resp.text();
try {
const json = JSON.parse(text);
throw new Error(json.msg || '完整报告生成失败');
} catch (error) {
if (error instanceof SyntaxError) throw new Error('完整报告生成失败,请稍后重试');
throw error;
}
}
return resp.blob();
}
+5 -2
View File
@@ -63,8 +63,11 @@ step "3/5 构建前端 ..."
# 4. 整理 runtime 目录
step "4/5 整理 runtime/wind_power/ ..."
rm -rf "${RUNTIME_DIR}"
mkdir -p "${RUNTIME_DIR}"/{config,web,libs,logs}
# data/ 是服务端运行期持久化目录,不能随打包清理,否则保存的方案和图表配置会丢失。
mkdir -p "${RUNTIME_DIR}"/{data,uploads,logs}
rm -rf "${RUNTIME_DIR}/config" "${RUNTIME_DIR}/web" "${RUNTIME_DIR}/libs"
rm -f "${RUNTIME_DIR}/wind_server"
mkdir -p "${RUNTIME_DIR}"/{config,web,libs}
cp "${PKG_BUILD}/wind_server" "${RUNTIME_DIR}/"
cp -r "${ROOT_DIR}"/backend/config/* "${RUNTIME_DIR}/config/"
+1
View File
@@ -0,0 +1 @@
custom: ["paypal.me/xlsxwriter"]
@@ -0,0 +1,85 @@
# libxlsxwriter: Reporting Bugs
Here are some tips on reporting bugs in `libxlsxwriter`.
### Upgrade to the latest version of the library
Upgrade to the latest version of the library since the bug you are reporting
may already be fixed.
Check the [Changes][changes] section of the documentation to see what has
changed in the latest versions.
[changes]: http://libxlsxwriter.github.io/changes.html
You can check which version of `libxlsxwriter` that you are using by checking
the `xlsxwriter.h` header file or by adding the following to your program:
```C
#include <stdio.h>
#include "xlsxwriter.h"
int main() {
printf("Libxlsxwriter version = %s\n", lxw_version());
return 0;
}
```
### Read the documentation
Read or search the `libxlsxwriter` [documentation][docs] to see if the issue
you are encountering is already explained.
[docs]: http://libxlsxwriter.github.io/index.html
### Look at the example programs
There are many [examples programs][examples] in the distribution. Try to
identify an example program that corresponds to your query and adapt it to use
as a bug report.
[examples]: http://libxlsxwriter.github.io/examples.html
### Tips for submitting a bug report
1. Describe the problem as clearly and as concisely as possible.
2. Include a sample program. This is probably the most important step.
It is generally easier to describe a problem in code than in written
prose.
3. The sample program should be as small as possible to demonstrate the
problem. Don't copy and paste large non-relevant sections of your
program.
A sample bug report is shown below. This format helps analyze and respond to
the bug report more quickly.
> Subject: Issue with SOMETHING
>
> Greetings,
>
> I am using libxlsxwriter to do SOMETHING but it appears to do SOMETHING ELSE.
>
> I am using CC version X.Y.Z, OS = uname and libxlsxwriter x.y.z.
>
> Here is some code that demonstrates the problem:
>
>
>```C
>#include "xlsxwriter.h"
>
>int main() {
>
> lxw_workbook *workbook = workbook_new("bug_report.xlsx");
> lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
>
> worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
> worksheet_write_number(worksheet, 1, 0, 123, NULL);
>
> return workbook_close(workbook);
>}
>```
>
@@ -0,0 +1,130 @@
# libxlsxwriter: Submitting Pull Requests
# Pull Requests and Contributing to Libxlsxwriter
All patches and pull requests are welcome but in general you should start with
an issue tracker to describe what you intend to do before you do it.
### Getting Started
1. Pull requests and new feature proposals must start with an [issue
tracker][issues]. This serves as the focal point for the design discussion.
2. Describe what you plan to do. If there are API changes add some code
example to demonstrate them.
3. Fork the repository.
4. Run all the tests to make sure the current code works on your system using
`make test`. See the [Running the Test Suite][tests] section of the docs
for instructions.
5. Create a feature branch for your new feature.
[tests]: http://libxlsxwriter.github.io/running_the_tests.html
### Code Style
The code style is mainly K&R style with 4 space indents.
The author uses GNU indent (`gindent`) 2.2.10 with the following options:
```
--braces-on-if-line
--braces-on-struct-decl-line
--case-indentation 4
--continue-at-parentheses
--declaration-comment-column 0
--format-first-column-comments
--honour-newlines
--ignore-profile
--indent-label 0
--indent-level 4
--no-space-after-function-call-names
--no-tabs
--swallow-optional-blank-lines
```
The [indent configuration file][indentpro] is available in the repo. The code
can be indented automatically if the same version of `gindent` is used with
the following make command:
```shell
make indent
```
Note, make sure you have backed up your files or added them to the index
before running this command.
In general follow the existing style in the code.
[indentpro]: https://github.com/jmcnamara/libxlsxwriter/blob/master/.indent.pro
### Writing and Running Tests
Any significant features should be accompanied by a test. See the `test`
directory and the [Running the Test Suite][tests] section of the docs for
details of the test setup.
The tests can be run as follows:
```shell
make test
```
Same as:
```shell
make test_unit
make test_functional
```
The functional tests require the Python module [pytest][pytest] as a test runner.
If you have `valgrind` installed you can use the test suite to check for memory leaks:
```shell
make test_valgrind
```
When you push your changes they will also be tested automatically using
[GitHub Actions][actions].
[actions]: https://github.com/jmcnamara/libxlsxwriter/actions
[pytest]: http://pytest.org/
### Documentation
The `libxlsxwriter` documentation is written in Doxygen format in the header
files and in additional `.dox` files in the `docs/src` directory of the
repo. The documentation can be built as follows:
```shell
make docs
open docs/html/index.html
```
### Example programs
If applicable add an example program to the `examples` directory. Example
files can be built using:
```shell
make docs
```
### Copyright and License
Copyright remains with the original author. Do not include additional
copyright claims or Licensing requirements. GitHub and the `git` repository
will record your contribution and it will be acknowledged it in the Changes
file.
### Submitting the Pull Request
If your change involves several incremental `git` commits then `rebase` or
`squash` them onto another branch so that the Pull Request is a single commit
or a small number of logical commits.
Push your changes to GitHub and submit the Pull Request with a hash link to
the to the Issue tracker that was opened above.
@@ -0,0 +1,46 @@
name: Build with CMake
on: [push, pull_request]
jobs:
build:
name:
Cmake
strategy:
matrix:
cc: [gcc, clang]
cmake_flags: ["",
"-DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON",
"-DUSE_DTOA_LIBRARY=ON -DBUILD_TESTS=ON",
"-DUSE_MEM_FILE=ON -DBUILD_TESTS=ON",
"-DUSE_NO_MD5=ON -DBUILD_TESTS=ON",
"-DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON",
"-DUSE_STANDARD_TMPFILE=ON -DBUILD_TESTS=ON",
"-DUSE_SYSTEM_MINIZIP=ON -DBUILD_TESTS=ON",
"-DUSE_SYSTEM_MINIZIP=ON -DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON"]
runs-on: ubuntu-latest
env:
CC: ${{ matrix.cc }}
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
sudo apt-get -y install zlib1g-dev
sudo apt-get -y install libminizip-dev
sudo apt-get -y install libssl-dev
- name: Configure CMake
working-directory: ${{ github.workspace }}/cmake
run: cmake .. -DBUILD_TESTS=ON ${{ matrix.cmake_flags }} -DCMAKE_BUILD_TYPE=Release
- name: Build
working-directory: ${{ github.workspace }}/cmake
run: cmake --build . --config Release --parallel
- name: Test
working-directory: ${{ github.workspace }}/cmake
run: ctest -C Release -V
@@ -0,0 +1,23 @@
name: Check code style
on: [push, pull_request]
jobs:
build:
name:
Check code style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
sudo apt-get -y install indent
sudo ln -s /usr/bin/indent /usr/bin/gindent
- name: Make indent
run: |
make indent
git status | grep 'nothing to commit'
@@ -0,0 +1,22 @@
name: Coverity Scan
on:
push:
branches: [coverity]
jobs:
coverity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build third party libs to exclude them from scan
run: make third_party
- uses: vapier/coverity-scan-action@v1
with:
project: libxlsxwriter
email: ${{ secrets.COVERITY_SCAN_EMAIL }}
token: ${{ secrets.COVERITY_SCAN_TOKEN }}
command: make -C src libxlsxwriter.a
@@ -0,0 +1,50 @@
name: Build with Make
on: [push, pull_request]
jobs:
build:
name:
Make
strategy:
matrix:
cc: [gcc, clang]
make_flags: ["",
"USE_STANDARD_TMPFILE=1",
"USE_SYSTEM_MINIZIP=1",
"USE_DTOA_LIBRARY=1",
"USE_NO_MD5=1",
"USE_OPENSSL_MD5=1",
"USE_MEM_FILE=1"]
runs-on: ubuntu-latest
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cc }}
CFLAGS: '-Werror'
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest
sudo apt-get -y install zlib1g-dev
sudo apt-get -y install libminizip-dev
sudo apt-get -y install libssl-dev
sudo apt-get -y install valgrind
- name: make
run: ${{ matrix.make_flags }} make V=1
- name: test unit
run: ${{ matrix.make_flags }} make test_unit V=1
- name: test functional
run: ${{ matrix.make_flags }} make test_functional V=1 -j
- name: test cpp
run: ${{ matrix.make_flags }} make test_cpp V=1
- name: test examples
run: ${{ matrix.make_flags }} make examples V=1
@@ -0,0 +1,22 @@
name: Test for memory leaks
on: [push, pull_request]
jobs:
build:
name:
Valgrind
runs-on: ubuntu-latest
env:
CC: gcc
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: |
sudo apt-get -y install valgrind
sudo apt-get -y install zlib1g-dev
- name: test valgrind
run: make test_valgrind V=1 -j 2
@@ -0,0 +1,52 @@
name: Cmake on Windows
on: [push, pull_request]
jobs:
build:
name: CMake on Windows
strategy:
matrix:
cmake_flags: ["-DBUILD_EXAMPLES=ON -DBUILD_TESTS=ON",
"-DUSE_DTOA_LIBRARY=ON -DBUILD_TESTS=ON",
"-DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON",
"-DUSE_SYSTEM_MINIZIP=ON -DBUILD_TESTS=ON",
"-DUSE_SYSTEM_MINIZIP=ON -DUSE_OPENSSL_MD5=ON -DBUILD_TESTS=ON"]
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: cmd
run: |
vcpkg.exe install zlib:x64-windows minizip:x64-windows openssl:x64-windows
vcpkg.exe integrate install
pip install pytest
- name: Configure CMake
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: cmd
run: |
cd cmake
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cmake .. -DCMAKE_BUILD_TYPE=Release ${{ matrix.cmake_flags }} -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -A x64
- name: Build
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: cmd
run: |
cd cmake
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
cmake --build . --config Release
- name: Test
working-directory: ${{env.GITHUB_WORKSPACE}}
shell: cmd
run: |
cd cmake
copy test\functional\src\Release\*.exe test\functional\src
pytest -v test/functional
+62
View File
@@ -0,0 +1,62 @@
*.a
*.o
*.so
*.so.*
*.to
*.lo
*.la
*.dylib
*.dll
*.gcno
*.gcda
test_*
!test_*.c
!test_*.cpp
!test_*.py
*.tar.gz
*~
TAGS
.#*
*#
~*xlsx
*.xlsx
*.bak
!test/functional/xlsx_files/*.xlsx
*.pyc
.pytest_cache/
.cproject
.project
.pydevproject
.settings/
.DS_Store
__pycache__
.cache
docs/html
docs/latex
.deps
.dirstamp
_temp.c
examples/*
!examples/*.c
!examples/*.png
!examples/Makefile
!examples/vbaProject.bin
cov-int
libxlsxwriter-coverity.tgz
build
third_party/zlib-1.2.8/configure.log
third_party/zlib-1.2.8/contrib/minizip/miniunz
third_party/zlib-1.2.8/contrib/minizip/minizip
third_party/zlib-1.2.8/example
third_party/zlib-1.2.8/examplesh
third_party/zlib-1.2.8/minigzip
third_party/zlib-1.2.8/minigzipsh
third_party/zlib-1.2.8/zlib.pc
cmake
!cmake/FindMINIZIP.cmake
!cmake/FindPackage.cmake
!cmake/i686-toolchain.cmake
.vscode
+157
View File
@@ -0,0 +1,157 @@
/*
* Indent rules for libxlsxwriter.
*
* The rules for user defined typedefs can be update as follows:
*
perl -i -pe 'print and last if /[l]ibxlsxwriter typedefs/' .indent.pro
ack -h typedef include/xlsxwriter/*.h src/*.c | perl -lne 'print "-T $1" if /\w+\s+\w+\s+(\w+)/' | sort >> .indent.pro
*
*/
/* Command line options used with GNU indent 2.2.10 */
--braces-on-if-line
--braces-on-struct-decl-line
--case-indentation 4
--continue-at-parentheses
--declaration-comment-column 0
--format-first-column-comments
--honour-newlines
--ignore-profile
--indent-label 0
--indent-level 4
--no-space-after-function-call-names
--no-tabs
--swallow-optional-blank-lines
/* Typedefs used in the code. */
-T int8_t
-T int16_t
-T int32_t
-T int64_t
-T uint8_t
-T uint16_t
-T uint32_t
-T uint64_t
-T ssize_t
-T size_t
-T time_t
-T LIST_ENTRY
-T RB_ENTRY
-T SLIST_ENTRY
-T STAILQ_ENTRY
-T TAILQ_ENTRY
/* libxlsxwriter typedefs. */
-T lxw_app
-T lxw_author_id
-T lxw_autofilter
-T lxw_border
-T lxw_button_options
-T lxw_cell
-T lxw_chart
-T lxw_chart_axis
-T lxw_chart_axis_display_unit
-T lxw_chart_axis_label_alignment
-T lxw_chart_axis_label_position
-T lxw_chart_axis_tick_mark
-T lxw_chart_axis_tick_position
-T lxw_chart_axis_type
-T lxw_chart_blank
-T lxw_chart_custom_label
-T lxw_chart_data_label
-T lxw_chart_error_bar_axis
-T lxw_chart_error_bar_cap
-T lxw_chart_error_bar_direction
-T lxw_chart_error_bar_type
-T lxw_chart_fill
-T lxw_chart_font
-T lxw_chart_gridline
-T lxw_chart_label_position
-T lxw_chart_label_separator
-T lxw_chart_legend
-T lxw_chart_legend_position
-T lxw_chart_line
-T lxw_chart_line_dash_type
-T lxw_chart_marker
-T lxw_chart_marker_type
-T lxw_chart_options
-T lxw_chart_pattern
-T lxw_chart_pattern_type
-T lxw_chart_point
-T lxw_chart_series
-T lxw_chart_title
-T lxw_chart_trendline_type
-T lxw_chart_type
-T lxw_chartsheet
-T lxw_chartsheet_name
-T lxw_col_options
-T lxw_col_t
-T lxw_color_t
-T lxw_comment
-T lxw_comment_options
-T lxw_cond_format_hash_element
-T lxw_cond_format_obj
-T lxw_conditional_format
-T lxw_content_types
-T lxw_core
-T lxw_custom
-T lxw_custom_property
-T lxw_data_val_obj
-T lxw_data_validation
-T lxw_datetime
-T lxw_defined_name
-T lxw_doc_properties
-T lxw_drawing
-T lxw_drawing_coords
-T lxw_drawing_object
-T lxw_drawing_rel_id
-T lxw_error
-T lxw_fill
-T lxw_filter_rule
-T lxw_filter_rule_obj
-T lxw_font
-T lxw_format
-T lxw_hash_element
-T lxw_hash_table
-T lxw_header_footer_options
-T lxw_heading_pair
-T lxw_image_md5
-T lxw_image_options
-T lxw_merged_range
-T lxw_metadata
-T lxw_object_properties
-T lxw_packager
-T lxw_panes
-T lxw_part_name
-T lxw_print_area
-T lxw_protection
-T lxw_protection_obj
-T lxw_rel_tuple
-T lxw_relationships
-T lxw_repeat_cols
-T lxw_repeat_rows
-T lxw_rich_string_tuple
-T lxw_row
-T lxw_row_col_options
-T lxw_row_t
-T lxw_selection
-T lxw_series_data_point
-T lxw_series_error_bars
-T lxw_series_range
-T lxw_sheet
-T lxw_sst
-T lxw_styles
-T lxw_table
-T lxw_table_column
-T lxw_table_obj
-T lxw_table_options
-T lxw_theme
-T lxw_tuple
-T lxw_vml
-T lxw_vml_obj
-T lxw_workbook
-T lxw_workbook_options
-T lxw_worksheet
-T lxw_worksheet_init_data
-T lxw_worksheet_name
+424
View File
@@ -0,0 +1,424 @@
# :copyright: (c) 2017 Alex Huszagh.
# :license: FreeBSD, see LICENSE.txt for more details.
# Description
# ===========
#
# Use:
# Move to a custom directory, ideally out of source, and
# type `cmake $LXW_SOURCE $FLAGS`, where `LXW_SOURCE` is the
# path to the libxlsxwriter project, and `FLAGS` are custom
# flags to pass to the compiler.
#
# Example:
# For example, in the project directory, to build libxlsxwriter
# and the unittests in release mode, type:
# mkdir build && cd build
# cmake .. -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
# cmake --build . --config Release
# ctest -C Release -V
# cmake --build . --config Release --target install
#
# If using a Makefile generator, you may use the simpler
# mkdir build && cd build
# cmake .. -DBUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Release
# make
# make test
# make install
#
# Flags:
# ZLIB_ROOT
# The ZLIB root directory can be specified either through
# an environment variable (`export ZLIB_ROOT=/usr/include`)
# or using a flag with CMake (`-DZLIB_ROOT:STRING=/usr/include`).
# This sets the preferred search path for the ZLIB installation.
#
# BUILD_TESTS
# Build unittests (default off). To build the unittests,
# pass `-DBUILD_TESTS=ON` during configuration.
#
# BUILD_EXAMPLES
# Build example files (default off). To build the examples,
# pass `-DBUILD_EXAMPLES=ON` during configuration.
#
# USE_STANDARD_TMPFILE
# Use the standard tmpfile() function (default off). To enable
# the standard tmpfile, pass `-DUSE_STANDARD_TMPFILE=ON`
# during configuration. This may produce bugs while cross-
# compiling or using MinGW/MSYS.
#
# USE_DTOA_LIBRARY
# Use the third party emyg_dtoa() library (default off). The
# emyg_dtoa() library is used to avoid sprintf double issues with
# different locale settings. To enable this library, pass
# `-DUSE_DTOA_LIBRARY=ON` during configuration.
#
# USE_NO_MD5
# Compile without third party MD5 support. This will turn off the
# functionality of avoiding duplicate image files in the output xlsx
# file. To enable this option pass `-DUSE_NO_MD5=ON` during
# configuration.
#
# USE_OPENSSL_MD5 Compile with OpenSSL MD5 support. This will link
# against libcrypto for MD5 support rather than using the local MD5
# support. MD5 support is required to avoid duplicate image files in
# the output xlsx file. To enable this option pass
# `-DUSE_OPENSSL_MD5=ON` during configuration.
#
# USE_STATIC_MSVC_RUNTIME
# Use the static msvc runtime library when compiling with msvc (default off)
# To enable, pass `-DUSE_STATIC_MSVC_RUNTIME` during configuration.
#
# Toolchains:
# On multiarch Linux systems, which can build and run multiple
# binary targets on the same system, we include an `i686-toolchain`
# file to enable building i686 (x86 32-bit) targets on x86_64 systems.
# To use the i686 toolchain, pass the `-DCMAKE_TOOLCHAIN_FILE` option
# during CMake configuration. For example, from the build directory,
# you would use:
# cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/i686-toolchain.cmake
#
# CMake Options:
# CMake sets debug and release builds with the `CMAKE_BUILD_TYPE`
# option, which can be set as a flag during configuration.
# To build in release mode, pass `-DCMAKE_BUILD_TYPE=Release`
# during configuration.
#
# CMake sets the creation of static and shared libraries with the
# `BUILD_SHARED_LIBS` option, which can be set as a flag during
# configuration. To build a static library, pass
# `-DBUILD_SHARED_LIBS=OFF` during configuration.
#
# Generators:
# CMake also supports custom build generators, such as MakeFiles,
# Ninja, Visual Studio, and XCode. For example, to generate
# a Visual Studio solution, configure with:
# cmake .. -G "Visual Studio 14 2015 Win64"
#
# For more information on using generators, see:
# https://cmake.org/cmake/help/v3.0/manual/cmake-generators.7.html
#
set(CMAKE_LEGACY_CYGWIN_WIN32 1)
if(MSVC)
cmake_minimum_required(VERSION 3.4)
else()
cmake_minimum_required(VERSION 3.1)
endif()
SET(XLSX_PROJECT_NAME "xlsxwriter" CACHE STRING "Optional project and binary name")
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
project(${XLSX_PROJECT_NAME} C)
enable_testing()
# POLICY
# ------
# The use of the word ZLIB_ROOT should still work prior to "3.12.0",
# just it's been generalized for all packages now. Just set the policy
# to new, so we use it, and it will be used prior to 3.12 anyway.
if(${CMAKE_VERSION} VERSION_GREATER "3.12" OR ${CMAKE_VERSION} VERSION_EQUAL "3.12")
cmake_policy(SET CMP0074 NEW)
endif()
# OPTIONS
# -------
SET(ZLIB_ROOT "" CACHE STRING "Optional root for the ZLIB installation")
option(BUILD_TESTS "Build libxlsxwriter tests" OFF)
option(BUILD_EXAMPLES "Build libxlsxwriter examples" OFF)
option(USE_SYSTEM_MINIZIP "Use system minizip installation" OFF)
option(USE_STANDARD_TMPFILE "Use the C standard library's tmpfile()" OFF)
option(USE_NO_MD5 "Build libxlsxwriter without third party MD5 lib" OFF)
option(USE_OPENSSL_MD5 "Build libxlsxwriter with the OpenSSL MD5 lib" OFF)
option(USE_MEM_FILE "Use fmemopen()/open_memstream() in place of temporary files" OFF)
option(IOAPI_NO_64 "Disable 64-bit filesystem support" OFF)
option(USE_DTOA_LIBRARY "Use the locale independent third party Milo Yip DTOA library" OFF)
if(MSVC)
option(USE_STATIC_MSVC_RUNTIME "Use the static runtime library" OFF)
endif()
if(DEFINED ENV{${ZLIB_ROOT}})
set(ZLIB_ROOT $ENV{ZLIB_ROOT})
endif()
if(IOAPI_NO_64)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS IOAPI_NO_64=1)
endif()
# CONFIGURATIONS
# --------------
if(USE_SYSTEM_MINIZIP)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_SYSTEM_MINIZIP)
endif()
if(USE_STANDARD_TMPFILE)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_STANDARD_TMPFILE)
endif()
if(NOT USE_OPENSSL_MD5 AND USE_NO_MD5)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_NO_MD5)
endif()
if(USE_OPENSSL_MD5)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_OPENSSL_MD5)
if(NOT MSVC)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-deprecated-declarations")
endif()
endif()
if(USE_MEM_FILE OR USE_FMEMOPEN)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_FMEMOPEN)
endif()
if(USE_DTOA_LIBRARY)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_DTOA_LIBRARY)
endif()
if(NOT BUILD_SHARED_LIBS)
if(UNIX)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
elseif(MINGW OR MSYS)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static -static-libgcc -Wno-char-subscripts -Wno-long-long")
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS USE_FILE32API)
elseif(MSVC)
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} /Ox /Zi /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
set(CMAKE_C_FLAGS_MINSIZEREL "${CMAKE_C_FLAGS_MINSIZEREL} /Zi /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
set(CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO} /Fd\"${CMAKE_BINARY_DIR}/${PROJECT_NAME}.pdb\"")
endif()
endif()
if(MSVC AND USE_STATIC_MSVC_RUNTIME)
foreach(flag_var CMAKE_C_FLAGS
CMAKE_C_FLAGS_DEBUG
CMAKE_C_FLAGS_RELEASE
CMAKE_C_FLAGS_MINSIZEREL
CMAKE_C_FLAGS_RELWITHDEBINFO)
if(${flag_var} MATCHES "/MD")
string(REPLACE "/MD" "/MT" ${flag_var} "${${flag_var}}")
endif()
endforeach()
endif()
# Configure pkg-config
file(READ "include/xlsxwriter.h" ver)
string(REGEX MATCH "LXW_VERSION \"([^\"]+)\"" _ ${ver})
set(VERSION ${CMAKE_MATCH_1})
string(REGEX MATCH "LXW_SOVERSION \"([^\"]+)\"" _ ${ver})
set(SOVERSION ${CMAKE_MATCH_1})
set(PREFIX ${CMAKE_INSTALL_PREFIX})
configure_file(dev/release/pkg-config.txt xlsxwriter.pc @ONLY)
# INCLUDES
# --------
enable_language(CXX)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake)
# ZLIB
find_package(ZLIB REQUIRED "1.0")
list(APPEND LXW_PRIVATE_INCLUDE_DIRS ${ZLIB_INCLUDE_DIRS})
message("zlib version: " ${ZLIB_VERSION})
# MINIZIP
if (USE_SYSTEM_MINIZIP)
find_package(MINIZIP REQUIRED "1.0")
list(APPEND LXW_PRIVATE_INCLUDE_DIRS ${MINIZIP_INCLUDE_DIRS})
endif()
# LIBRARY
# -------
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS NOCRYPT NOUNCRYPT)
# Ensure CRT Secure warnings are disabled
if(MSVC)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS _CRT_SECURE_NO_WARNINGS)
endif()
# Ensure "TESTING" macro is defined if building tests
if(BUILD_TESTS)
list(APPEND LXW_PRIVATE_COMPILE_DEFINITIONS TESTING)
endif()
file(GLOB LXW_SOURCES src/*.c)
file(GLOB_RECURSE LXW_HEADERS RELATIVE include *.h)
if(NOT USE_SYSTEM_MINIZIP)
list(APPEND LXW_SOURCES third_party/minizip/ioapi.c third_party/minizip/zip.c)
if(MSVC)
list(APPEND LXW_SOURCES third_party/minizip/iowin32.c)
endif()
endif()
if (NOT USE_STANDARD_TMPFILE)
list(APPEND LXW_SOURCES third_party/tmpfileplus/tmpfileplus.c)
endif()
if(NOT USE_OPENSSL_MD5 AND NOT USE_NO_MD5)
list(APPEND LXW_SOURCES third_party/md5/md5.c)
endif()
if(USE_OPENSSL_MD5)
find_package(OpenSSL REQUIRED)
if(OpenSSL_FOUND)
include_directories(${OPENSSL_INCLUDE_DIR})
message(STATUS "OpenSSL version: ${OPENSSL_VERSION}")
endif()
endif()
if (USE_DTOA_LIBRARY)
list(APPEND LXW_SOURCES third_party/dtoa/emyg_dtoa.c)
endif()
set(LXW_PROJECT_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(LXW_LIB_DIR "${LXW_PROJECT_DIR}/lib")
add_library(${PROJECT_NAME} "")
set_target_properties(${PROJECT_NAME} PROPERTIES SOVERSION ${SOVERSION})
target_sources(${PROJECT_NAME}
PRIVATE ${LXW_SOURCES}
PUBLIC ${LXW_HEADERS}
)
target_link_libraries(${PROJECT_NAME} LINK_PUBLIC ${ZLIB_LIBRARIES} ${MINIZIP_LIBRARIES} ${LIB_CRYPTO} ${OPENSSL_CRYPTO_LIBRARY})
target_compile_definitions(${PROJECT_NAME} PRIVATE ${LXW_PRIVATE_COMPILE_DEFINITIONS})
# /utf-8 needs VS2015 Update 2 or above.
# In CMake 3.7 and above, we can use (MSVC_VERSION GREATER_EQUAL 1900) here.
if(MSVC AND NOT (MSVC_VERSION LESS 1900))
target_compile_options(${PROJECT_NAME} PRIVATE /utf-8)
endif()
if (WINDOWSSTORE)
target_compile_definitions(${PROJECT_NAME} PRIVATE -DIOWIN32_USING_WINRT_API)
endif()
target_include_directories(${PROJECT_NAME}
PRIVATE ${LXW_PRIVATE_INCLUDE_DIRS}
PUBLIC include include/xlsxwriter
)
# TESTS
# -----
# Create test and runner.
#
# Args:
# sources Name of variable holding source files
# target Test name
#
macro(CreateTest sources target)
set(output_name xlsxwriter_${target})
set(dependencies ${output_name})
add_executable(${output_name} ${${sources}})
target_link_libraries(${output_name} ${PROJECT_NAME})
target_compile_definitions(${output_name} PRIVATE TESTING COLOR_OK)
add_test(NAME ${output_name}
COMMAND ${output_name}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
endmacro(CreateTest)
file(GLOB LXW_UTILITY_SOURCES test/unit/utility/test*.c)
file(GLOB LXW_XMLWRITER_SOURCES test/unit/xmlwriter/test*.c)
file(GLOB LXW_WORKSHEET_SOURCES test/unit/worksheet/test*.c)
file(GLOB LXW_SST_SOURCES test/unit/sst/test*.c)
file(GLOB LXW_WORKBOOK_SOURCES test/unit/workbook/test*.c)
file(GLOB LXW_APP_SOURCES test/unit/app/test*.c)
file(GLOB LXW_CONTENTTYPES_SOURCES test/unit/content_types/test*.c)
file(GLOB LXW_CORE_SOURCES test/unit/core/test*.c)
file(GLOB LXW_RELATIONSHIPS_SOURCES test/unit/relationships/test*.c)
file(GLOB LXW_FORMAT_SOURCES test/unit/format/test*.c)
file(GLOB LXW_STYLES_SOURCES test/unit/styles/test*.c)
file(GLOB LXW_DRAWING_SOURCES test/unit/drawing/test*.c)
file(GLOB LXW_CHART_SOURCES test/unit/chart/test*.c)
file(GLOB LXW_CUSTOM_SOURCES test/unit/custom/test*.c)
file(GLOB LXW_FUNCTIONAL_SOURCES test/functional/src/*.c)
set(LXW_UNIT_SOURCES
test/unit/test_all.c
${LXW_UTILITY_SOURCES}
${LXW_XMLWRITER_SOURCES}
${LXW_WORKSHEET_SOURCES}
${LXW_SST_SOURCES}
${LXW_WORKBOOK_SOURCES}
${LXW_APP_SOURCES}
${LXW_CONTENTTYPES_SOURCES}
${LXW_CORE_SOURCES}
${LXW_RELATIONSHIPS_SOURCES}
${LXW_FORMAT_SOURCES}
${LXW_STYLES_SOURCES}
${LXW_DRAWING_SOURCES}
${LXW_CHART_SOURCES}
${LXW_CUSTOM_SOURCES}
)
if(BUILD_TESTS)
# unit tests
CreateTest(LXW_UNIT_SOURCES unit)
# functional tests
find_package(Python COMPONENTS Interpreter REQUIRED)
find_program(Pytest_EXECUTABLE NAMES pytest)
if (NOT Pytest_EXECUTABLE)
message("Please install the Python pytest library to run functional tests:")
message(" pip install pytest\n")
endif()
foreach(source ${LXW_FUNCTIONAL_SOURCES})
get_filename_component(basename ${source} NAME_WE)
add_executable(${basename} ${source})
target_link_libraries(${basename} xlsxwriter)
set_target_properties(${basename} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "test/functional/src")
endforeach(source)
add_custom_command(TARGET xlsxwriter_unit POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/test/functional test/functional
)
if(USE_NO_MD5)
add_test(NAME functional
COMMAND pytest -v test/functional -m "not skipif"
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
else()
add_test(NAME functional
COMMAND pytest -v test/functional
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
endif()
endif()
# EXAMPLES
# --------
file(GLOB LXW_EXAMPLE_SOURCES examples/*.c)
if(BUILD_EXAMPLES)
foreach(source ${LXW_EXAMPLE_SOURCES})
get_filename_component(basename ${source} NAME_WE)
add_executable(${basename} ${source})
target_link_libraries(${basename} ${PROJECT_NAME})
set_target_properties(${basename} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "examples")
endforeach(source)
endif()
# INSTALL
# -------
include(GNUInstallDirs)
install(TARGETS ${PROJECT_NAME}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
install(FILES include/xlsxwriter.h DESTINATION include)
install(DIRECTORY include/xlsxwriter
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.h"
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/xlsxwriter.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
+226
View File
@@ -0,0 +1,226 @@
# libxlsxwriter: Reporting Bugs and submitting Pull Requests
## Reporting Bugs
Here are some tips on reporting bugs in `libxlsxwriter`.
### Upgrade to the latest version of the library
Upgrade to the latest version of the library since the bug you are reporting
may already be fixed.
Check the [Changes][changes] section of the documentation to see what has
changed in the latest versions.
[changes]: http://libxlsxwriter.github.io/changes.html
You can check which version of `libxlsxwriter` that you are using by checking
the `xlsxwriter.h` header file or by adding the following to your program:
```C
#include <stdio.h>
#include "xlsxwriter.h"
int main() {
printf("Libxlsxwriter version = %s\n", lxw_version());
return 0;
}
```
### Read the documentation
Read or search the `libxlsxwriter` [documentation][docs] to see if the issue
you are encountering is already explained.
[docs]: http://libxlsxwriter.github.io/index.html
### Look at the example programs
There are many [examples programs][examples] in the distribution. Try to
identify an example program that corresponds to your query and adapt it to use
as a bug report.
[examples]: http://libxlsxwriter.github.io/examples.html
### Use the xlsxwriter Issue Tracker
The [libxlsxwriter issue tracker][issues] is on GitHub.
[issues]: https://github.com/jmcnamara/libxlsxwriter/issues
### Tips for submitting a bug report
1. Describe the problem as clearly and as concisely as possible.
2. Include a sample program. This is probably the most important step.
It is generally easier to describe a problem in code than in written
prose.
3. The sample program should be as small as possible to demonstrate the
problem. Don't copy and paste large non-relevant sections of your
program.
A sample bug report is shown below. This format helps analyze and respond to
the bug report more quickly.
> Subject: Issue with SOMETHING
>
> Greetings,
>
> I am using libxlsxwriter to do SOMETHING but it appears to do SOMETHING ELSE.
>
> I am using CC version X.Y.Z, OS = uname and libxlsxwriter x.y.z.
>
> Here is some code that demonstrates the problem:
>
>
>```C
>#include "xlsxwriter.h"
>
>int main() {
>
> lxw_workbook *workbook = workbook_new("bug_report.xlsx");
> lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
>
> worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
> worksheet_write_number(worksheet, 1, 0, 123, NULL);
>
> return workbook_close(workbook);
>}
>```
>
# Pull Requests and Contributing to Libxlsxwriter
All patches and pull requests are welcome but in general you should start with
an issue tracker to describe what you intend to do before you do it.
### Getting Started
1. Pull requests and new feature proposals must start with an [issue
tracker][issues]. This serves as the focal point for the design discussion.
2. Describe what you plan to do. If there are API changes add some code
example to demonstrate them.
3. Fork the repository.
4. Run all the tests to make sure the current code works on your system using
`make test`. See the [Running the Test Suite][tests] section of the docs
for instructions.
5. Create a feature branch for your new feature.
[tests]: http://libxlsxwriter.github.io/running_the_tests.html
### Code Style
The code style is mainly K&R style with 4 space indents.
The author uses GNU indent (`gindent`) 2.2.10 with the following options:
```
--braces-on-if-line
--braces-on-struct-decl-line
--case-indentation 4
--continue-at-parentheses
--declaration-comment-column 0
--format-first-column-comments
--honour-newlines
--ignore-profile
--indent-label 0
--indent-level 4
--no-space-after-function-call-names
--no-tabs
--swallow-optional-blank-lines
```
The [indent configuration file][indentpro] is available in the repo. The code
can be indented automatically if the same version of `gindent` is used with
the following make command:
```shell
make indent
```
Note, make sure you have backed up your files or added them to the index
before running this command.
In general follow the existing style in the code.
[indentpro]: https://github.com/jmcnamara/libxlsxwriter/blob/master/.indent.pro
### Writing and Running Tests
Any significant features should be accompanied by a test. See the `test`
directory and the [Running the Test Suite][tests] section of the docs for
details of the test setup.
The tests can be run as follows:
```shell
make test
```
Same as:
```shell
make test_unit
make test_functional
```
The functional tests require the Python module [pytest][pytest] as a test runner.
If you have `valgrind` installed you can use the test suite to check for memory leaks:
```shell
make test_valgrind
```
When you push your changes they will also be tested automatically using
[GitHub Actions][actions].
[actions]: https://github.com/jmcnamara/libxlsxwriter/actions
[pytest]: http://pytest.org/
### Documentation
The `libxlsxwriter` documentation is written in Doxygen format in the header
files and in additional `.dox` files in the `docs/src` directory of the
repo. The documentation can be built as follows:
```shell
make docs
open docs/html/index.html
```
### Example programs
If applicable add an example program to the `examples` directory. Example
files can be built using:
```shell
make docs
```
### Copyright and License
Copyright remains with the original author. Do not include additional
copyright claims or Licensing requirements. GitHub and the `git` repository
will record your contribution and it will be acknowledged it in the Changes
file.
### Submitting the Pull Request
If your change involves several incremental `git` commits then `rebase` or
`squash` them onto another branch so that the Pull Request is a single commit
or a small number of logical commits.
Push your changes to GitHub and submit the Pull Request with a hash link to
the to the Issue tracker that was opened above.
File diff suppressed because it is too large Load Diff
+208
View File
@@ -0,0 +1,208 @@
/**
@page license License
Libxlsxwriter is released under a FreeBSD license:
Copyright 2014-2022, John McNamara <jmcnamara@cpan.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are
those of the authors and should not be interpreted as representing
official policies, either expressed or implied, of the FreeBSD Project.
Libxlsxwriter includes the `queue.h` and `tree.h` macros from FreeBSD. It also
includes and, unless overridden, uses the optional libraries `minizip`,
`tmpfileplus` and `md5`. It also includes the `emyg_dtoa` library but doesn't
use it by default. These components have the following licenses:
Queue.h from FreeBSD:
Copyright (c) 1991, 1993
The Regents of the University of California. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
4. Neither the name of the University nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
Tree.h from FreeBSD:
Copyright 2002 Niels Provos <provos@citi.umich.edu>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The `minizip` files used in the libxlsxwriter source tree are taken from the
`zlib` ` contrib/minizip` directory. [Zlib](http://www.zlib.net) has the
following License/Copyright:
(C) 1995-2013 Jean-loup Gailly and Mark Adler
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
Jean-loup Gailly Mark Adler
jloup@gzip.org madler@alumni.caltech.edu
The `minizip` files have the following additional copyright declarations:
Copyright (C) 1998-2010 Gilles Vollant
(minizip) ( http://www.winimage.com/zLibDll/minizip.html )
Modifications for Zip64 support
Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
Note, it is possible to compile libxlsxwriter without statically linking the
`minizip` files and instead dynamically linking to `lminizip`, see
@ref gsg_minizip.
[Tmpfileplus](http://www.di-mgt.com.au/c_function_to_create_temp_file.html)
has the following license:
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) 2012-16 David Ireland, DI Management Services Pty Ltd
<http://www.di-mgt.com.au/contact/>.
See the [Mozilla Public License, v. 2.0](http://mozilla.org/MPL/2.0/).
Note, it is possible to compile libxlsxwriter using the standard library
`tmpfile()` function instead of `tmpfileplus`, see @ref gsg_tmpdir.
The [Milo Yip DTOA library](https://github.com/miloyip/dtoa-benchmark) for
converting doubles to strings. It has the following license:
Copyright (C) 2015 Doug Currie
based on dtoa_milo.h
Copyright (C) 2014 Milo Yip
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
This Milo Yip DTOA library (emyg_dtoa) is used to avoid issues where the
standard sprintf() dtoa function changes output based on locale settings. It
is also 40-50% faster than the standard dtoa for raw numeric data. The use of
this library is optional. If you wish to use it you can pass
`USE_DTOA_LIBRARY=1` to make when compiling.
[Openwall MD5](https://openwall.info/wiki/people/solar/software/public-domain-source-code/md5)
has the following licence:
This software was written by Alexander Peslyak in 2001. No copyright is
claimed, and the software is hereby placed in the public domain.
In case this attempt to disclaim copyright and place the software in the
public domain is deemed null and void, then the software is
Copyright (c) 2001 Alexander Peslyak and it is hereby released to the
general public under the following terms:
Redistribution and use in source and binary forms, with or without
modification, are permitted.
There's ABSOLUTELY NO WARRANTY, express or implied.
(This is a heavily cut-down "BSD license".)
Note, the MD5 library is used to avoid including duplicate image files in the
xlsx file. If you don't want to use this code, and the additional licence, you
can use OpenSSL's MD5 functions instead by passing `USE_OPENSSL_MD5=1` to
make. If this functionality isn't required it is possible to compile
libxlsxwriter without image deduplication by passing `USE_NO_MD5=1` to make.
See also @ref gsg_md5.
Next: @ref changes
*/
+275
View File
@@ -0,0 +1,275 @@
###############################################################################
#
# Makefile for libxlsxwriter library.
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
# Keep the output quiet by default.
Q=@
ifdef V
Q=
endif
DESTDIR ?=
PREFIX ?= /usr/local
PYTEST ?= py.test
PYTESTFILES ?= test
VERSION = $(shell sed -n -e 's/.*LXW_VERSION \"\(.*\)\"/\1/p' include/xlsxwriter.h)
SOVERSION = $(shell sed -n -e 's/.*LXW_SOVERSION \"\(.*\)\"/\1/p' include/xlsxwriter.h)
.PHONY: docs tags examples third_party
# Build libxlsxwriter.
all : third_party
$(Q)$(MAKE) -C src
# Build the third party libs.
third_party :
ifndef USE_SYSTEM_MINIZIP
$(Q)$(MAKE) -C third_party/minizip
endif
ifndef USE_STANDARD_TMPFILE
$(Q)$(MAKE) -C third_party/tmpfileplus
endif
ifndef USE_NO_MD5
ifndef USE_OPENSSL_MD5
$(Q)$(MAKE) -C third_party/md5
endif
endif
ifdef USE_DTOA_LIBRARY
$(Q)$(MAKE) -C third_party/dtoa
endif
# Build a macOS universal binary.
universal_binary :
$(Q)$(MAKE) clean
$(Q)TARGET_ARCH="-target x86_64-apple-macos10.12" $(MAKE) all
$(Q)mv lib/libxlsxwriter.a libxlsxwriter_x86_64.a
$(Q)mv lib/libxlsxwriter.$(SOVERSION).dylib libxlsxwriter_x86_64.dylib
$(Q)$(MAKE) clean
$(Q)TARGET_ARCH="-target arm64-apple-macos11" $(MAKE) all
$(Q)mv lib/libxlsxwriter.a lib/libxlsxwriter_arm64.a
$(Q)mv lib/libxlsxwriter.$(SOVERSION).dylib lib/libxlsxwriter_arm64.dylib
$(Q)mv libxlsxwriter_x86_64.a libxlsxwriter_x86_64.dylib lib
$(Q)lipo -create -output lib/libxlsxwriter.a lib/libxlsxwriter_x86_64.a lib/libxlsxwriter_arm64.a
$(Q)lipo -create -output lib/libxlsxwriter.$(SOVERSION).dylib lib/libxlsxwriter_x86_64.dylib lib/libxlsxwriter_arm64.dylib
$(Q)rm -f lib/libxlsxwriter_x86_64.* lib/libxlsxwriter_arm64.*
# Build the example programs.
examples : all
$(Q)$(MAKE) -C examples
# Clean src and test directories.
clean :
$(Q)$(MAKE) clean -C src
$(Q)$(MAKE) clean -C test/unit
$(Q)$(MAKE) clean -C test/functional/src
$(Q)$(MAKE) clean -C test/cpp
$(Q)$(MAKE) clean -C examples
$(Q)rm -rf docs/html
$(Q)rm -rf test/functional/__pycache__
$(Q)rm -f test/functional/*.pyc
$(Q)rm -f lib/*
$(Q)$(MAKE) clean -C third_party/minizip
$(Q)$(MAKE) clean -C third_party/tmpfileplus
$(Q)$(MAKE) clean -C third_party/md5
$(Q)$(MAKE) clean -C third_party/dtoa
# Clean src and lib dir only, as a precursor for static analysis.
clean_src :
$(Q)$(MAKE) clean -C src
$(Q)rm -f lib/*
# Run the unit tests.
test : all test_cpp test_unit test_functional
# Test for C++ const correctness on APIs.
test_const : all
$(Q)$(MAKE) clean -C test/functional/src
$(Q)! $(MAKE) -C test/functional/src CFLAGS=-Wwrite-strings 2>&1 | grep -A 1 "note:"
# Run the functional tests.
test_functional : all
$(Q)$(MAKE) -C test/functional/src
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
# Run all tests.
test_unit : all
$(Q)$(MAKE) -C src test_lib
$(Q)$(MAKE) -C test/unit test
# Test C++ compilation.
test_cpp : all
$(Q)$(MAKE) -C test/cpp
# Test Cmake. This test should really be done with Cmake in the cmake dir but
# this is a workaround for now.
test_cmake :
ifneq ($(findstring m32,$(CFLAGS)),m32)
$(Q)$(MAKE) -C src clean
$(Q)cd cmake; cmake .. -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON; make clean; make; cp libxlsxwriter.a ../src/
$(Q)cmake/xlsxwriter_unit
$(Q)$(MAKE) -C test/functional/src
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
else
@echo "Skipping Cmake tests on 32 bit target."
endif
# Test the functional test exes with valgrind (in 64bit mode only).
test_valgrind : all
ifndef NO_VALGRIND
$(Q)$(MAKE) -C test/functional/src test_valgrind
$(Q)$(MAKE) -C examples test_valgrind
endif
# Minimal target for quick compile without creating the libs.
test_compile :
$(Q)$(MAKE) -C src test_compile
# Indent the source files with the .indent.pro settings.
indent:
$(Q)gindent src/*.c include/*.h include/xlsxwriter/*.h
tags:
$(Q)rm -f TAGS
$(Q)etags src/*.c include/*.h include/xlsxwriter/*.h
# Build the doxygen docs.
doc: docs
docs:
$(Q)$(MAKE) -C docs
@echo "Docs built."
docs_doxygen_only:
$(Q)$(MAKE) -C docs docs_doxygen_only
docs_external:
$(Q)make -C ../libxlsxwriter.github.io release
# Simple install.
install: all
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/include
$(Q)cp -R include/* $(DESTDIR)$(PREFIX)/include
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib
$(Q)cp -R lib/* $(DESTDIR)$(PREFIX)/lib
$(Q)mkdir -p $(DESTDIR)$(PREFIX)/lib/pkgconfig
$(Q)sed -e 's|@PREFIX@|$(PREFIX)|g' -e 's|@VERSION@|$(VERSION)|g' dev/release/pkg-config.txt > $(DESTDIR)$(PREFIX)/lib/pkgconfig/xlsxwriter.pc
# Simpler uninstall.
uninstall:
$(Q)rm -rf $(DESTDIR)$(PREFIX)/include/xlsxwriter*
$(Q)rm $(DESTDIR)$(PREFIX)/lib/libxlsxwriter.*
$(Q)rm $(DESTDIR)$(PREFIX)/lib/pkgconfig/xlsxwriter.pc
# Strip the lib files.
strip:
$(Q)strip lib/*
# Run a coverity static analysis.
coverity: clean_src third_party
$(Q)rm -rf cov-int
$(Q)rm -f libxlsxwriter-coverity.tgz
$(Q)../../cov-analysis-linux64-2019.03/bin/cov-build --dir cov-int make -C src libxlsxwriter.a
$(Q)tar -czf libxlsxwriter-coverity.tgz cov-int
$(Q)$(MAKE) -C src clean
$(Q)rm -f lib/*
# Run gcov coverage analysis.
gcov: third_party
$(Q)$(MAKE) -C src clean
$(Q)$(MAKE) -C src GCOV="--coverage" OPT_LEVEL="-O0"
$(Q)$(MAKE) -C src test_lib GCOV="--coverage"
$(Q)$(MAKE) -C test/unit test GCOV="--coverage"
$(Q)$(MAKE) -C test/functional/src GCOV="--coverage"
$(Q)$(PYTEST) test/functional -v -k $(PYTESTFILES)
$(Q)mkdir -p build
$(Q)gcovr -r src --html-details -o build/libxlsxwriter_gcov.html
$(Q)gcovr -r . -f src --sonarqube build/coverage.xml
# Run sonarcloud analysis.
sonarcloud: gcov
ifndef SONAR_TOKEN
@echo "Please define SONAR_TOKEN to run this analysis."
@exit 1
endif
$(Q)$(MAKE) clean
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/build-wrapper-macosx-x86 --out-dir build make all
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/sonar-scanner \
-Dsonar.organization=jmcnamara-github \
-Dsonar.projectKey=jmcnamara_libxlsxwriter \
-Dsonar.projectName=libxlsxwriter \
-Dsonar.projectVersion=$(VERSION) \
-Dsonar.sources=src \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.cfamily.build-wrapper-output=build \
-Dsonar.working.directory=build/scannerwork \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.cfamily.threads=4 \
-Dsonar.coverageReportPaths=build/coverage.xml \
-Dsonar.cfamily.cache.enabled=false
sonarcloud_no_gcov:
ifndef SONAR_TOKEN
@echo "Please define SONAR_TOKEN to run this analysis."
@exit 1
endif
$(Q)$(MAKE) clean
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/build-wrapper-macosx-x86 --out-dir build make all
$(Q)../sonar-scanner-4.6.1.2450-macosx/bin/sonar-scanner \
-Dsonar.organization=jmcnamara-github \
-Dsonar.projectKey=jmcnamara_libxlsxwriter \
-Dsonar.projectName=libxlsxwriter \
-Dsonar.projectVersion=$(VERSION) \
-Dsonar.sources=src \
-Dsonar.sourceEncoding=UTF-8 \
-Dsonar.cfamily.build-wrapper-output=build \
-Dsonar.working.directory=build/scannerwork \
-Dsonar.host.url=https://sonarcloud.io \
-Dsonar.cfamily.threads=4 \
-Dsonar.cfamily.cache.enabled=false
# Run a scan-build static analysis.
scan_build: clean_src third_party
$(Q)scan-build make -C src libxlsxwriter.a
$(Q)$(MAKE) -C src clean
$(Q)rm -f lib/*
spellcheck:
$(Q)for f in docs/src/*.dox; do aspell --lang=en_US --check $$f; done
$(Q)for f in include/xlsxwriter/*.h; do aspell --lang=en_US --check $$f; done
$(Q)for f in src/*.c; do aspell --lang=en_US --check $$f; done
$(Q)for f in examples/*.c; do aspell --lang=en_US --check $$f; done
$(Q)aspell --lang=en_US --check Changes.txt
$(Q)aspell --lang=en_US --check Readme.md
$(Q)aspell --lang=en_US --check docs/src/examples.txt
releasecheck:
$(Q)dev/release/release_check.sh
release: releasecheck
@echo
@echo "Pushing to git main ..."
$(Q)git push origin main
$(Q)git push --tags
@echo
@echo "Pushing updated docs ..."
$(Q)make -C ../libxlsxwriter.github.io release
@echo
@echo "Pushing the cocoapod ..."
$(Q)pod trunk push libxlsxwriter.podspec --use-libraries
@echo
@echo "Finished. Opening files."
$(Q)open https://libxlsxwriter.github.io/changes.html
$(Q)open https://cocoadocs.org/docsets/libxlsxwriter
$(Q)open https://github.com/jmcnamara/libxlsxwriter
$(Q)open https://github.com/jmcnamara/libxlsxwriter/releases
+82
View File
@@ -0,0 +1,82 @@
# libxlsxwriter
Libxlsxwriter: A C library for creating Excel XLSX files.
![demo image](http://libxlsxwriter.github.io/demo.png)
## The libxlsxwriter library
Libxlsxwriter is a C library that can be used to write text, numbers, formulas
and hyperlinks to multiple worksheets in an Excel 2007+ XLSX file.
It supports features such as:
- 100% compatible Excel XLSX files.
- Full Excel formatting.
- Merged cells.
- Defined names.
- Autofilters.
- Charts.
- Data validation and drop down lists.
- Conditional formatting.
- Worksheet PNG/JPEG/GIF images.
- Cell comments.
- Support for adding Macros.
- Memory optimization mode for writing large files.
- Source code available on [GitHub](https://github.com/jmcnamara/libxlsxwriter).
- FreeBSD license.
- ANSI C.
- Works with GCC, Clang, Xcode, MSVC 2015, ICC, TCC, MinGW, MingGW-w64/32.
- Works on Linux, FreeBSD, OpenBSD, OS X, iOS and Windows. Also works on MSYS/MSYS2 and Cygwin.
- Compiles for 32 and 64 bit.
- Compiles and works on big and little endian systems.
- The only dependency is on `zlib`.
Here is an example that was used to create the spreadsheet shown above:
```C
#include "xlsxwriter.h"
int main() {
/* Create a new workbook and add a worksheet. */
lxw_workbook *workbook = workbook_new("demo.xlsx");
lxw_worksheet *worksheet = workbook_add_worksheet(workbook, NULL);
/* Add a format. */
lxw_format *format = workbook_add_format(workbook);
/* Set the bold property for the format */
format_set_bold(format);
/* Change the column width for clarity. */
worksheet_set_column(worksheet, 0, 0, 20, NULL);
/* Write some simple text. */
worksheet_write_string(worksheet, 0, 0, "Hello", NULL);
/* Text with formatting. */
worksheet_write_string(worksheet, 1, 0, "World", format);
/* Write some numbers. */
worksheet_write_number(worksheet, 2, 0, 123, NULL);
worksheet_write_number(worksheet, 3, 0, 123.456, NULL);
/* Insert an image. */
worksheet_insert_image(worksheet, 1, 2, "logo.png");
workbook_close(workbook);
return 0;
}
```
See the [full documentation](http://libxlsxwriter.github.io) for the getting
started guide, a tutorial, the main API documentation and examples.
@@ -0,0 +1,37 @@
#import <Foundation/Foundation.h>
#import "../xlsxwriter.h"
#import "app.h"
#import "chart.h"
#import "chartsheet.h"
#import "comment.h"
#import "common.h"
#import "content_types.h"
#import "core.h"
#import "custom.h"
#import "drawing.h"
#import "format.h"
#import "hash_table.h"
#import "metadata.h"
#import "packager.h"
#import "relationships.h"
#import "shared_strings.h"
#import "styles.h"
#import "table.h"
#import "theme.h"
#import "third_party/emyg_dtoa.h"
#import "third_party/ioapi.h"
#import "third_party/md5.h"
#import "third_party/queue.h"
#import "third_party/tmpfileplus.h"
#import "third_party/tree.h"
#import "third_party/zip.h"
#import "utility.h"
#import "vml.h"
#import "workbook.h"
#import "worksheet.h"
#import "xmlwriter.h"
FOUNDATION_EXPORT double xlsxwriterVersionNumber;
FOUNDATION_EXPORT const unsigned char xlsxwriterVersionString[];
@@ -0,0 +1,7 @@
framework module xlsxwriter {
umbrella header "xlsxwriter/libxlsxwriter-umbrella.h"
header "xlsxwriter.h"
export *
module * { export * }
}
+4
View File
@@ -0,0 +1,4 @@
This directory contains some release utilities that are mainly useful
to the library developer.
+7
View File
@@ -0,0 +1,7 @@
#/bin/bash
# Perform some minor clean-ups/fixes to the docs.
perl -i -pe "s/_page/_8h/" html/pages.html
perl -i ../dev/release/fix_example_docs.pl html/examples.html
cp menudata.js html
@@ -0,0 +1,127 @@
#!/usr/bin/perl
#
# Simple program to arrange the example programs in a user defined order
# instead of a sorted order. Also add a caption.
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
use warnings;
use strict;
# The required example order and descriptions.
my @examples = (
[ 'hello.c', 'A simple hello world example' ],
[ 'anatomy.c', 'The anatomy of a libxlsxwriter program' ],
[ 'demo.c', 'Demo of some of the libxlsxwriter features' ],
[ 'tutorial1.c', 'Tutorial 1 from the documentation' ],
[ 'tutorial2.c', 'Tutorial 2 from the documentation' ],
[ 'tutorial3.c', 'Tutorial 3 from the documentation' ],
[ 'format_font.c', 'Example of writing data with font formatting' ],
[ 'format_num_format.c', 'Example of writing data with number formatting' ],
[ 'dates_and_times01.c', 'Writing dates and times with numbers' ],
[ 'dates_and_times02.c', 'Writing dates and times with datetime' ],
[ 'dates_and_times03.c', 'Writing dates and times with Unix datetimes' ],
[ 'dates_and_times04.c', 'Dates and times with different formats' ],
[ 'hyperlinks.c', 'A example of writing urls/hyperlinks' ],
[ 'rich_strings.c', 'A example of writing "rich" multi-format strings' ],
[ 'array_formula.c', 'A example of using array formulas' ],
[ 'dynamic_arrays.c', 'A example of using Excel 365 dynamic array formulas' ],
[ 'utf8.c', 'A example of some UTF-8 text' ],
[ 'constant_memory.c', 'Write a large file with constant memory usage' ],
[ 'output_buffer.c', 'Write a file to a memory buffer' ],
[ 'image_buffer.c', 'Example of adding an image from a memory buffer.' ],
[ 'merge_range.c', 'Create a merged range of cells' ],
[ 'merge_rich_string.c', 'Create a merged range with a rich string' ],
[ 'autofilter.c', 'An example of a worksheet autofilter' ],
[ 'data_validate.c', 'Examples of worksheet data validation and drop down lists' ],
[ 'conditional_format1.c', 'A simple conditional formatting example' ],
[ 'conditional_format2.c', 'An advanced conditional formatting example' ],
[ 'tables.c', 'Example of table to a worksheet.' ],
[ 'images.c', 'Example of adding images to a worksheet.' ],
[ 'headers_footers.c', 'Example of adding worksheet headers/footers' ],
[ 'defined_name.c', 'Example of how to create defined names' ],
[ 'outline.c', 'Example of grouping and outlines' ],
[ 'outline_collapsed.c', 'Example of grouping and collapsed outlines' ],
[ 'watermark.c', 'Example of how to set a watermark image for a worksheet' ],
[ 'background.c', 'Example of how to set the background image for a worksheet' ],
[ 'tab_colors.c', 'Example of how to set worksheet tab colors' ],
[ 'diagonal_border.c', 'Example of how to set a worksheet cell diagonal border.' ],
[ 'hide_sheet.c', 'Example of hiding a worksheet' ],
[ 'doc_properties.c', 'Example of setting workbook doc properties' ],
[ 'doc_custom_properties.c','Example of setting custom doc properties' ],
[ 'worksheet_protection.c', 'Example of enabling worksheet protection' ],
[ 'hide_row_col.c', 'Example of hiding worksheet rows and columns' ],
[ 'comments1.c', 'Example of adding cell comments to a worksheet' ],
[ 'comments2.c', 'Example of adding cell comments with options' ],
[ 'macro.c', 'Example of adding a VBA macro to a workbook' ],
[ 'panes.c', 'Example of how to create worksheet panes' ],
[ 'ignore_errors.c', 'Example of ignoring worksheet errors/warnings' ],
[ 'lambda.c', 'Example of using the EXCEL 365+ LAMBDA() function' ],
[ 'chart.c', 'Example of a simple column chart' ],
[ 'chart_area.c', 'Examples of area charts' ],
[ 'chart_bar.c', 'Examples of bar charts' ],
[ 'chart_column.c', 'Examples of column charts' ],
[ 'chart_line.c', 'Example of a line chart' ],
[ 'chart_scatter.c', 'Examples of scatter charts' ],
[ 'chart_radar.c', 'Examples of radar charts' ],
[ 'chart_pie.c', 'Examples of pie charts' ],
[ 'chart_doughnut.c', 'Examples of doughnut charts' ],
[ 'chart_clustered.c', 'Examples of clustered category chart' ],
[ 'chart_data_table.c', 'Examples of charts with data tables' ],
[ 'chart_data_tools.c', 'Examples of charts data tools' ],
[ 'chart_data_labels.c', 'Examples of charts data labels' ],
[ 'chart_fonts.c', 'Examples of using charts fonts' ],
[ 'chart_pattern.c', 'Examples of using charts patterns' ],
[ 'chart_styles.c', 'Examples of built-in charts styles' ],
[ 'chartsheet.c', 'Example of a chartsheet chart' ],
);
# Convert the array refs to a hash for lookups.
my %examples;
for my $example (@examples) {
$examples{$example->[0]} = 1;
}
my $in_list = 0;
while ( my $line = <> ) {
# Print all lines not in the <ul> list.
print $line if !$in_list;
# Check for <ul> list.
if ( $line =~ /<div class="textblock">/ ) {
$in_list = 1;
}
# Capture the <li> items of the list.
if ( $line =~ /<li><a class="el" href="[^"]+">([^<]+)/ ) {
my $example = $1;
# Warn if there are any new/unkown items.
if ( !exists $examples{$example} ) {
warn "$0 Unknown example: $example\n";
}
next;
}
# At the end of the <ul> list print out the <li> items in user defined order.
if ( $line =~ m{^</ul>} ) {
$in_list = 0;
for my $aref ( @examples ) {
my $example = $aref->[0];
my $filename = $aref->[0];
my $desc = $aref->[1];
$example =~ s/\.c/_8c-example.html/;
printf qq(<li><a class="el" href="%s">%s</a> %s</li>\n\n),
$example, $filename, $desc;
}
print $line;
}
}
__END__
@@ -0,0 +1,78 @@
#!/usr/bin/perl
#
# Simple program to generate the examples.dox file from a simple text file,
# with links to the next/previous examples.
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
use strict;
use warnings;
my @examples;
my @sections;
my @links;
my $buffer = '';
# Sample through the example sections and break the text into blocks.
while ( my $line = <> ) {
# Ignore comments in the input file.
next if $line =~ /^#/;
# Match the start of an example block.
if ( $line =~ /^\@example/ ) {
chomp $buffer;
# Store the example name and the section body.
push @examples, $line;
push @sections, $buffer;
$buffer = '';
next;
}
$buffer .= $line;
}
# Store the last example section and omit the first blank element.
push @sections, $buffer;
shift @sections;
# Generate a set of @ref links targets from the example program names.
for ( @examples ) {
my $link = $_;
chomp $link;
$link =~ s/\@example //;
push @links, [ $link, $link ];
}
# Add the first and last links back to the examples.
unshift @links, [ "examples", "Examples page" ];
push @links, [ "examples", "Examples page" ];
# Add the start of the Doxygen header.
print "/**\n";
print "\@page examples Example Programs\n\n";
# Print out each section.
for my $i ( 0 .. @examples - 1 ) {
print $examples[$i];
# Add a simple header table with next/previous links.
printf qq{\n<table width="600">\n};
printf qq{<tr>\n};
printf qq{ <td>\@ref %s "&lt;&lt; %s"</td>\n},
$links[$i]->[0], $links[$i]->[1];
printf qq{ <td align="right">\@ref %s "%s &gt;&gt;"</td>\n},
$links[ $i + 2 ]->[0], $links[ $i + 2 ]->[1];
printf qq{</tr>\n};
printf qq{</table>\n};
print $sections[$i], "\n\n\n\n";
}
# Print the end of the doxygen comment.
print "*/\n";
@@ -0,0 +1,49 @@
#!/usr/bin/perl
#
# Simple program to generate the string array for the lxw_strerror() function
# from the Doxygen comments in the lxw_error enum:
#
# perl dev/release/gen_error_strings.pl include/xlsxwriter/common.h
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
use warnings;
use strict;
my $in_enum = 0;
my @strings;
my $filename = shift || 'include/xlsxwriter/common.h';
open my $fh, '<', $filename or die "Couldn't open $filename: $!\n";
while (<$fh>) {
$in_enum = 1 if /typedef enum lxw_error/;
$in_enum = 0 if /} lxw_error;/;
# Match doxygen strings in the enum.
if ($in_enum && m{/\*\*}) {
# Strip the comment parts.
s{/\*\*}{};
s{\*/}{};
s{^\s+}{};
s{\s+$}{};
push @strings, $_;
}
}
# Print out an array of strings based on the doxygen comments.
print "\n";
print "// Copy to src/utility.c\n\n";
print "char *error_strings[LXW_MAX_ERRNO + 1] = {\n";
for my $string (@strings) {
print qq{ "$string",\n};
}
print qq{ "Unknown error number."\n};
print "};\n\n";
@@ -0,0 +1,36 @@
#!/usr/bin/perl
#
# Simple program to generate the coccoapods unbrella file.
# Run from the libxlsxwriter root dir.
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
use warnings;
use strict;
use File::Find;
my @includes;
# Callback to match header files.
sub match_include {
push @includes, $File::Find::name if /^.*\.h\z/s;
}
# Use File::Find to find header files.
find({wanted => \&match_include}, 'include/xlsxwriter');
# Sort and remove leading dirs from the include files.
@includes = sort @includes;
s{^include/xlsxwriter/}{} for @includes;
# Generate the unbrella file.
print qq{#import <Foundation/Foundation.h>\n\n};
print qq{#import "../xlsxwriter.h"\n};
print qq{#import "$_"\n} for @includes;
print qq{\n};
print qq{FOUNDATION_EXPORT double xlsxwriterVersionNumber;\n};
print qq{FOUNDATION_EXPORT const unsigned char xlsxwriterVersionString[];\n\n};
@@ -0,0 +1,50 @@
#!/usr/bin/perl
#
# Simple program to generate a Windows .def file from the exported symbols in
# libxlsxwriter.a.
#
# perl dev/release/gen_windows_def_file.pl lib/libxlsxwriter.a
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
use warnings;
use strict;
my $lib_file = shift;
die "$0: Path to .a lib file required.\n" if !$lib_file;
die "$0: File '$lib_file' not found\n" if !-e $lib_file;
# Get the symbols from the libxlsxwriter.a file.
my @symbols = `nm $lib_file`;
my %unique;
for my $symbol ( @symbols ) {
chomp $symbol;
# Get the last field in the row.
my @fields = split " ", $symbol;
$symbol = $fields[-1];
next unless $symbol;
# Skip symbols not belonging to libxlsxwriter.
next if $symbol !~ /^_(lxw|work|format|chart|new)/;
# Skip some the RedBlack functions.
next if $symbol =~ m{RB};
# Strip the leading underscore.
$symbol =~ s/^_//;
# Remove duplicate instances of some symbols.
$unique{$symbol}++;
}
# Generate the .def file.
print "EXPORTS\r\n";
for my $symbol ( sort keys %unique ) {
print " ", $symbol, "\r\n";
}
@@ -0,0 +1,10 @@
prefix=@PREFIX@
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib
Name: libxlsxwriter
Description: A C library for creating Excel XLSX files
Version: @VERSION@
Cflags: -I${includedir}
Libs: -L${libdir} -lxlsxwriter -lz
+332
View File
@@ -0,0 +1,332 @@
#!/bin/bash
clear
echo "|"
echo "| Pre-release checks."
echo "|"
echo
#############################################################
#
# Run tests.
#
function check_test_status {
echo
echo -n "Are all tests passing? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Run all tests now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: make test\n";
exit 1
else
echo " Running tests...";
make test
check_test_status
fi
fi
}
#############################################################
#
# Run test for C++ const correctness.
#
function check_test_const {
echo
echo -n "Is the const test passing? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Run test now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: make test_const\n";
exit 1
else
echo " Running test...";
make test_const
check_test_const
fi
fi
}
#############################################################
#
# Run spellcheck.
#
function check_spellcheck {
echo
echo -n "Is the spellcheck ok? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Run spellcheck now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: make spellcheck\n";
exit 1
else
echo " Running spellcheck...";
make spellcheck
check_spellcheck
fi
fi
}
#############################################################
#
# Check Changes file is up to date.
#
function check_changefile {
clear
echo "Latest change in Changes file: "
perl -ne '$rev++ if /^##/; exit if $rev > 1; print " | $_"' Changes.txt
echo
echo -n "Is the Changes file updated? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please update the Change file to proceed.\n";
exit 1
fi
}
#############################################################
#
# Check the versions are up to date.
#
function check_versions {
clear
echo
echo "Latest file versions: "
echo
awk '/s.version / {print "\t" FILENAME "\t" $1 "\t" $3}' libxlsxwriter.podspec
awk '/ LXW/ {print "\t" FILENAME "\t" $2 "\t" $3}' include/xlsxwriter.h
echo
echo -n "Are the versions up to date? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Update versions? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please update the versions to proceed.\n";
exit 1
else
echo " Updating versions...";
perl -i dev/release/update_revison.pl include/xlsxwriter.h libxlsxwriter.podspec
check_versions
fi
fi
}
#############################################################
#
# Check that the docs build cleanly.
#
function check_docs {
# clear
echo
echo -n "Do the docs build cleanly? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Build docs now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: make docs\n";
exit 1
else
echo " Building docs...";
make docs
check_docs
fi
fi
}
#############################################################
#
# Generate the cocoapods umbrella file.
#
function gen_umbrella_file {
echo
echo -n "Is the umbrella file up to date? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Update umbrella file now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please update cocoapods/libxlsxwriter-umbrella.h\n";
exit 1
else
echo " Updating file...";
perl dev/release/gen_umbrella_file.pl > cocoapods/libxlsxwriter-umbrella.h
fi
fi
}
#############################################################
#
# Check the cocoapods spec file.
#
function check_pod_spec {
echo
echo -n "Is the coacoapod file ok? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Run lint now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: pod spec lint libxlsxwriter.podspec\n";
exit 1
else
echo " Running lint...";
pod spec lint libxlsxwriter.podspec --use-libraries
check_pod_spec
fi
fi
}
#############################################################
#
# Update the pod repo. This can take some time.
#
function update_pod_repo {
echo
echo -n "Is the pod repo updated? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo -n " Update now? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please run: pod spec lint libxlsxwriter.podspec\n";
exit 1
else
echo " Running update...";
cd ~/.cocoapods/repos/master
git pull --ff-only
cd -
update_pod_repo
fi
fi
}
#############################################################
#
# Run release checks.
#
function check_git_status {
clear
echo "Git status: "
git status | awk '{print " | ", $0}'
echo "Git log: "
git log -1 | awk '{print " | ", $0}'
echo "Git latest tag: "
git tag -l -n1 | tail -1 | awk '{print " | ", $0}'
echo
echo -n "Is the git status okay? [y/N]: "
read RESPONSE
if [ "$RESPONSE" != "y" ]; then
echo
echo -e "Please fix git status.\n";
echo -e "\ngit add -u";
git tag -l -n1 | tail -1 | perl -lane 'printf "git commit -m \"Prep for release %s\"\ngit tag \"%s\"\n\n", $F[4], $F[0]' | perl dev/release/update_revison.pl
exit 1
fi
}
check_test_status
clear
check_test_const
clear
check_spellcheck
clear
check_docs
check_changefile
clear
gen_umbrella_file
check_pod_spec
clear
update_pod_repo
check_versions
check_git_status
#############################################################
#
# All checks complete.
#
clear
echo
echo "Everything is configured.";
echo
echo -n "Confirm release: [y/N]: ";
read RESPONSE
if [ "$RESPONSE" == "y" ]; then
exit 0
else
exit 1
fi
@@ -0,0 +1,42 @@
#!/usr/bin/perl
# Simple script to increment x.y.z style version numbers in a file.
use strict;
use warnings;
use Perl::Version;
while (<>) {
# Increment any x.y.z version strings.
if (m/(\d\.\d\.\d)/) {
my $version = Perl::Version->new( $1 );
# Components are: revision, version and subversion.
if ( $version->version == 9 && $version->subversion == 9 ) {
$version->inc_revision();
}
elsif ( $version->subversion == 9 ) {
$version->inc_version();
}
else {
$version->inc_subversion();
}
my $new_version = $version->stringify();
s/\d\.\d\.\d/$new_version/;
}
# Increment the LXW_VERSION_ID number in xlsxwriter.h
if (m/LXW_VERSION_ID (\d+)/) {
my $version = $1;
my $new_version = $version + 1;
s/\d+/$new_version/;
}
print;
}
__END__
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
<doxygenlayout version="1.0">
<!-- Generated by doxygen 1.8.7 -->
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="modules" visible="yes" title="" intro=""/>
<tab type="pages" visible="yes" title="" intro=""/>
<tab type="files" visible="yes" title="">
<tab type="filelist" visible="yes" title="" intro=""/>
</tab>
<tab type="examples" visible="yes" title="" intro="Example programs using libxlsxwriter:"/>
</navindex>
<!-- Layout definition for a class page -->
<class>
<briefdescription visible="yes"/>
<detaileddescription title=""/>
<inheritancegraph visible="$CLASS_GRAPH"/>
<collaborationgraph visible="$COLLABORATION_GRAPH"/>
<memberdecl>
<nestedclasses visible="yes" title=""/>
<publictypes title=""/>
<services title=""/>
<interfaces title=""/>
<publicslots title=""/>
<signals title=""/>
<publicmethods title=""/>
<publicstaticmethods title=""/>
<publicattributes title=""/>
<publicstaticattributes title=""/>
<protectedtypes title=""/>
<protectedslots title=""/>
<protectedmethods title=""/>
<protectedstaticmethods title=""/>
<protectedattributes title=""/>
<protectedstaticattributes title=""/>
<packagetypes title=""/>
<packagemethods title=""/>
<packagestaticmethods title=""/>
<packageattributes title=""/>
<packagestaticattributes title=""/>
<properties title=""/>
<events title=""/>
<privatetypes title=""/>
<privateslots title=""/>
<privatemethods title=""/>
<privatestaticmethods title=""/>
<privateattributes title=""/>
<privatestaticattributes title=""/>
<friends title=""/>
<related title="" subtitle=""/>
<membergroups visible="yes"/>
</memberdecl>
<memberdef>
<inlineclasses title=""/>
<typedefs title=""/>
<enums title=""/>
<services title=""/>
<interfaces title=""/>
<constructors title=""/>
<functions title=""/>
<related title=""/>
<variables title=""/>
<properties title=""/>
<events title=""/>
</memberdef>
<allmemberslink visible="yes"/>
<usedfiles visible="$SHOW_USED_FILES"/>
<authorsection visible="yes"/>
</class>
<!-- Layout definition for a namespace page -->
<namespace>
<briefdescription visible="yes"/>
<memberdecl>
<nestednamespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<classes visible="yes" title=""/>
<typedefs title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<inlineclasses title=""/>
<typedefs title=""/>
<enums title=""/>
<functions title=""/>
<variables title=""/>
</memberdef>
<authorsection visible="yes"/>
</namespace>
<!-- Layout definition for a file page -->
<file>
<detaileddescription title="Description"/>
<sourcelink visible="yes"/>
<memberdecl>
<functions title=""/>
</memberdecl>
<memberdef>
<functions title=""/>
<inlineclasses title=""/>
<typedefs title=""/>
<defines title=""/>
<enums title=""/>
<variables title=""/>
</memberdef>
<memberdecl>
<classes visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<constantgroups visible="yes" title=""/>
<typedefs title=""/>
<variables title=""/>
<enums title=""/>
<defines title=""/>
<membergroups visible="yes"/>
</memberdecl>
<authorsection/>
</file>
<!-- Layout definition for a group page -->
<group>
<briefdescription visible="yes"/>
<groupgraph visible="$GROUP_GRAPHS"/>
<memberdecl>
<nestedgroups visible="yes" title=""/>
<dirs visible="yes" title=""/>
<files visible="yes" title=""/>
<namespaces visible="yes" title=""/>
<classes visible="yes" title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
<membergroups visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
<memberdef>
<pagedocs/>
<inlineclasses title=""/>
<defines title=""/>
<typedefs title=""/>
<enums title=""/>
<enumvalues title=""/>
<functions title=""/>
<variables title=""/>
<signals title=""/>
<publicslots title=""/>
<protectedslots title=""/>
<privateslots title=""/>
<events title=""/>
<properties title=""/>
<friends title=""/>
</memberdef>
<authorsection visible="yes"/>
</group>
<!-- Layout definition for a directory page -->
<directory>
<briefdescription visible="yes"/>
<directorygraph visible="yes"/>
<memberdecl>
<dirs visible="yes"/>
<files visible="yes"/>
</memberdecl>
<detaileddescription title=""/>
</directory>
</doxygenlayout>
+28
View File
@@ -0,0 +1,28 @@
###############################################################################
#
# Makefile for libxlsxwriter library.
#
# Copyright 2014-2022, John McNamara, jmcnamara@cpan.org
#
# Keep the output quiet by default.
Q=@
ifdef V
Q=
endif
# Make everything.
all : docs
# Clean up.
clean :
$(Q)rm -rf html/*
# Build the doxygen docs.
docs:
$(Q)perl ../dev/release/fix_example_links.pl src/examples.txt > src/examples.dox
$(Q)doxygen
$(Q)../dev/release/fix_dox.sh
docs_doxygen_only:
$(Q)doxygen
File diff suppressed because it is too large Load Diff
+18
View File
@@ -0,0 +1,18 @@
<!-- HTML footer for doxygen 1.8.20-->
<!-- start footer part -->
<!--BEGIN GENERATE_TREEVIEW-->
<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
<ul>
$navpath
<li class="footer">Copyright 2014-2022 John McNamara. $generatedby <a href="http://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
</ul>
</div>
<!--END GENERATE_TREEVIEW-->
<!--BEGIN !GENERATE_TREEVIEW-->
<hr class="footer"/><address class="footer"><small>
Copyright 2014-2022 John McNamara.
$generatedby&#160;<a href="http://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
</small></address>
<!--END !GENERATE_TREEVIEW-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Some files were not shown because too many files have changed in this diff Show More