From c1ddf9259cd17aacd6cfb5d95f2109ad7e5459d5 Mon Sep 17 00:00:00 2001 From: Michael Maertzdorf Date: Thu, 27 Aug 2026 20:58:10 +0200 Subject: [PATCH] fix: harden fixed-width band padding Keep reduced-paper fixed-width bands deterministic and add warning-clean regression coverage across supported build modes. Follow-up to #23. --- .dockerignore | 7 + .gitattributes | 1 + .github/workflows/build.yml | 12 + .gitignore | 1 + AUTHORS | 1 + ChangeLog | 13 + README.md | 30 ++ include/band_layout.h | 30 ++ include/io_utils.h | 15 + module.mk | 6 + src/band.cpp | 21 +- src/band_layout.cpp | 50 +++ src/bandplane.cpp | 47 ++- src/cache.cpp | 4 +- src/compress.cpp | 471 +------------------------- src/compress_banded.inc | 143 ++++++++ src/compress_dispatch.inc | 35 ++ src/compress_jbig.inc | 182 ++++++++++ src/compress_widths.inc | 104 ++++++ src/document.cpp | 4 +- src/io_utils.cpp | 49 +++ src/module.mk | 1 + src/page.cpp | 73 +--- src/page_io.inc | 69 ++++ src/pstoqpdl.cpp | 13 +- src/qpdl.cpp | 9 +- tests/Dockerfile | 21 ++ tests/fixed_bandwidth_ppd_test.py | 50 +++ tests/fixed_bandwidth_test.cpp | 130 +++++++ tests/io_utils_test.cpp | 29 ++ tests/raster_fixture.cpp | 70 ++++ tests/run_docker_matrix.sh | 43 +++ tests/run_fixed_bandwidth_ppd_test.sh | 24 ++ tests/run_fixed_bandwidth_tests.sh | 49 +++ tests/run_protocol_output_test.sh | 43 +++ tests/serialization_test.cpp | 75 ++++ 36 files changed, 1351 insertions(+), 574 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 include/band_layout.h create mode 100644 include/io_utils.h create mode 100644 src/band_layout.cpp create mode 100644 src/compress_banded.inc create mode 100644 src/compress_dispatch.inc create mode 100644 src/compress_jbig.inc create mode 100644 src/compress_widths.inc create mode 100644 src/io_utils.cpp create mode 100644 src/page_io.inc create mode 100644 tests/Dockerfile create mode 100755 tests/fixed_bandwidth_ppd_test.py create mode 100644 tests/fixed_bandwidth_test.cpp create mode 100644 tests/io_utils_test.cpp create mode 100644 tests/raster_fixture.cpp create mode 100755 tests/run_docker_matrix.sh create mode 100755 tests/run_fixed_bandwidth_ppd_test.sh create mode 100755 tests/run_fixed_bandwidth_tests.sh create mode 100644 tests/run_protocol_output_test.sh create mode 100644 tests/serialization_test.cpp diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..00bb496d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.build +.deps +.worktrees +debug +default +optimized diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..dfdb8b77 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ff3c7d1..0ef3eda6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,20 @@ name: Build SpliX on: push: paths: + - 'include/**' + - 'ppd/**' - 'src/**' + - 'tests/**' + - 'Makefile' + - 'module.mk' pull_request: paths: + - 'include/**' + - 'ppd/**' - 'src/**' + - 'tests/**' + - 'Makefile' + - 'module.mk' workflow_dispatch: jobs: @@ -62,8 +72,10 @@ jobs: # force libcupsimage dynamic linking, as newer cups on newer ubuntu links it from libcups sed -i 's~rastertoqpdl_LIBS.*:= ~rastertoqpdl_LIBS := -lcupsimage ~' module.mk grep 'libjbig' module.mk + sh tests/run_docker_matrix.sh make DRV_ONLY=1 -j4 make ppd + python3 tests/fixed_bandwidth_ppd_test.py ppd rm -rf optimized/src mv ppd optimized/ diff --git a/.gitignore b/.gitignore index 2d0a4db2..7b3a35e7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ .gdb_history gmon.out .defs.mk +.build/ optimized ppd/*.drv tmp diff --git a/AUTHORS b/AUTHORS index 84fe0278..42e81bf5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ Aurélien Croc Till Kamppeter ValdikSS +Michael Maertzdorf diff --git a/ChangeLog b/ChangeLog index 09308eb1..2ff21d01 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,16 @@ +Fri Sep 4 12:00:00 2026 Michael Maertzdorf + + * Make reduced-paper fixed-width bands fully deterministic by using the + protocol bandwidth as the destination stride and zeroing all padding. + * Split compression responsibilities into focused implementation units. + * Add sanitizer-backed band-layout regression tests and generated-PPD + characterization for all fixed-bandwidth printer models. + * Make compiler diagnostics fatal in test builds, fix the warnings they + exposed, and preserve ML-1520 and unaffected ML-2010 A5 QPDL output + with golden tests. + * Check page, band, and plane swap-file I/O so truncation and short I/O + fail safely without leaking descriptors. + Thu Jul 16 21:00:00 2026 ValdikSS --- RELEASE OF THE VERSION V. 2.0.2 --- diff --git a/README.md b/README.md index b8d8f7a5..14ddda2f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,36 @@ Note that older SPL1-based models (ML-12xx, ML-14xx) do not work. Use these prin See installation instructions in the INSTALL file. +### Fixed-bandwidth protocol handling + +Some older SPL2 printers require every compressed row to use the full +4960-pixel protocol width, even on A5 and other reduced paper sizes. SpliX keeps +the raster data left-aligned and zero-fills the remaining band area for those +models. The reusable layout code lives in `src/band_layout.cpp`; the compression +entry point is split into focused `src/compress_*.inc` implementation units. + +Run the focused regression and generated-PPD characterization tests with: + +```sh +sh tests/run_fixed_bandwidth_tests.sh +sh tests/run_fixed_bandwidth_ppd_test.sh +sh tests/run_protocol_output_test.sh +``` + +The first command requires a C++23 compiler and runs with AddressSanitizer and +UndefinedBehaviorSanitizer. The second requires `ppdc` and Python 3. The third +generates an A5 CUPS raster and verifies byte-for-byte QPDL output for both the +fixed-width ML-1520 and unaffected variable-width ML-2010. + +Run the complete normal, no-thread, no-JBIG, and sanitizer matrix in Debian +stable with: + +```sh +docker build -f tests/Dockerfile -t splix-tests . +docker run --rm -v "$PWD:/src" -w /src splix-tests \ + sh tests/run_docker_matrix.sh +``` + The driver was created by Aurélien Croc (aurelien at ap2c dot org) and contains many contributions from Till Kamppeter (till dot kamppeter at gmail dot com). Development is discontinued as most modern printers do not need drivers any more. ### Supported models diff --git a/include/band_layout.h b/include/band_layout.h new file mode 100644 index 00000000..3a1dea25 --- /dev/null +++ b/include/band_layout.h @@ -0,0 +1,30 @@ +/* + * Band layout helpers for SpliX fixed-width printer protocols. + * + * Copyright (C) 2026 Michael Maertzdorf + * SPDX-License-Identifier: GPL-2.0-only + */ +#ifndef SPLIX_BAND_LAYOUT_H +#define SPLIX_BAND_LAYOUT_H + +namespace splix { + +enum BandLayout { + RowMajor, + ColumnMajor, +}; + +unsigned long fixedBandWidthBytes(unsigned long xResolution); + +void copyBandRows(unsigned char* band, + unsigned long bandWidthInBytes, + unsigned long bandHeight, + const unsigned char* source, + unsigned long sourceStride, + unsigned long sourceOffset, + unsigned long rows, + BandLayout layout); + +} // namespace splix + +#endif /* SPLIX_BAND_LAYOUT_H */ diff --git a/include/io_utils.h b/include/io_utils.h new file mode 100644 index 00000000..65428672 --- /dev/null +++ b/include/io_utils.h @@ -0,0 +1,15 @@ +/* Checked POSIX I/O helpers. Copyright (C) 2026 Michael Maertzdorf. */ +#ifndef SPLIX_IO_UTILS_H +#define SPLIX_IO_UTILS_H + +#include + +namespace splix { + +bool readAll(int fd, void* buffer, std::size_t size); +bool writeAll(int fd, const void* buffer, std::size_t size); +bool canRead(int fd, std::size_t size); + +} // namespace splix + +#endif /* SPLIX_IO_UTILS_H */ diff --git a/module.mk b/module.mk index f9881a44..1c26df4b 100644 --- a/module.mk +++ b/module.mk @@ -25,6 +25,7 @@ DISABLE_JBIG ?= 0 DISABLE_THREADS ?= 0 DISABLE_BLACKOPTIM ?= 0 DRV_ONLY ?= 0 +WARNINGS_AS_ERRORS ?= 0 # Flags @@ -36,6 +37,10 @@ rastertoqpdl_LIBS := `pkg-config --libs cups` -lcupsimage pstoqpdl_LDFLAGS := $(LDFLAGS) pstoqpdl_LIBS := `pkg-config --libs cups` -lcupsimage +ifneq ($(WARNINGS_AS_ERRORS),0) +CXXFLAGS += -Werror +endif + # Update compilation flags with defined options ifneq ($(DISABLE_THREADS),0) @@ -76,4 +81,5 @@ src_pstoqpdl_cpp_FLAGS += -DPSTORASTER=\"$(PSTORASTER)\" src_pstoqpdl_cpp_FLAGS += -DGSTORASTER=\"$(GSTORASTER)\" src_pstoqpdl_cpp_FLAGS += -DCUPSPPD=\"$(CUPSPPD)\" src_pstoqpdl_cpp_FLAGS += -DCUPSPROFILE=\"$(CUPSPROFILE)\" +src_ppdfile_cpp_FLAGS += -Wno-deprecated-declarations diff --git a/src/band.cpp b/src/band.cpp index e8f7633b..c3044ebc 100644 --- a/src/band.cpp +++ b/src/band.cpp @@ -22,6 +22,7 @@ #include #include "errlog.h" #include "bandplane.h" +#include "io_utils.h" /* * Constructeur - Destructeur @@ -71,10 +72,11 @@ Band::~Band() */ bool Band::swapToDisk(int fd) { - write(fd, &_bandNr, sizeof(_bandNr)); - write(fd, &_colors, sizeof(_colors)); - write(fd, &_width, sizeof(_width)); - write(fd, &_height, sizeof(_height)); + if (!splix::writeAll(fd, &_bandNr, sizeof(_bandNr)) || + !splix::writeAll(fd, &_colors, sizeof(_colors)) || + !splix::writeAll(fd, &_width, sizeof(_width)) || + !splix::writeAll(fd, &_height, sizeof(_height))) + return false; for (unsigned int i=0; i < _colors; i++) if (!_planes[i]->swapToDisk(fd)) return false; @@ -87,10 +89,13 @@ Band* Band::restoreIntoMemory(int fd) Band* band; band = new Band(); - read(fd, &band->_bandNr, sizeof(band->_bandNr)); - read(fd, &colors, sizeof(colors)); - read(fd, &band->_width, sizeof(band->_width)); - read(fd, &band->_height, sizeof(band->_height)); + if (!splix::readAll(fd, &band->_bandNr, sizeof(band->_bandNr)) || + !splix::readAll(fd, &colors, sizeof(colors)) || colors > 4 || + !splix::readAll(fd, &band->_width, sizeof(band->_width)) || + !splix::readAll(fd, &band->_height, sizeof(band->_height))) { + delete band; + return NULL; + } for (unsigned int i=0; i < colors; i++) { BandPlane *plane = BandPlane::restoreIntoMemory(fd); if (!plane) { diff --git a/src/band_layout.cpp b/src/band_layout.cpp new file mode 100644 index 00000000..ca1f8d7b --- /dev/null +++ b/src/band_layout.cpp @@ -0,0 +1,50 @@ +/* + * Band layout helpers for SpliX fixed-width printer protocols. + * + * Copyright (C) 2026 Michael Maertzdorf + * SPDX-License-Identifier: GPL-2.0-only + */ +#include "band_layout.h" + +#include +#include +#include + +namespace splix { + +unsigned long fixedBandWidthBytes(unsigned long xResolution) +{ + const unsigned long fixedWidthAt600Dpi = 4960; + const unsigned long bitsPerByte = 8; + return xResolution * fixedWidthAt600Dpi / 600 / bitsPerByte; +} + +void copyBandRows(unsigned char* band, + unsigned long bandWidthInBytes, + unsigned long bandHeight, + const unsigned char* source, + unsigned long sourceStride, + unsigned long sourceOffset, + unsigned long rows, + BandLayout layout) +{ + std::memset(band, 0, bandWidthInBytes * bandHeight); + if (sourceOffset >= sourceStride) + return; + + const unsigned long copyWidth = std::min( + sourceStride - sourceOffset, bandWidthInBytes); + const unsigned long copyRows = std::min(rows, bandHeight); + + for (unsigned long y = 0; y < copyRows; ++y) { + for (unsigned long x = 0; x < copyWidth; ++x) { + const unsigned long destination = + layout == ColumnMajor + ? x * bandHeight + y + : y * bandWidthInBytes + x; + band[destination] = source[y * sourceStride + sourceOffset + x]; + } + } +} + +} // namespace splix diff --git a/src/bandplane.cpp b/src/bandplane.cpp index 93bec114..5c08cace 100644 --- a/src/bandplane.cpp +++ b/src/bandplane.cpp @@ -19,6 +19,9 @@ * */ #include "bandplane.h" +#include "io_utils.h" +#include +#include #include /* @@ -65,13 +68,12 @@ void BandPlane::setData(unsigned char *data, unsigned long size) */ bool BandPlane::swapToDisk(int fd) { - write(fd, &_colorNr, sizeof(_colorNr)); - write(fd, &_size, sizeof(_size)); - write(fd, _data, _size); - write(fd, &_checksum, sizeof(_checksum)); - write(fd, &_endian, sizeof(_endian)); - write(fd, &_compression, sizeof(_compression)); - return true; + return splix::writeAll(fd, &_colorNr, sizeof(_colorNr)) && + splix::writeAll(fd, &_size, sizeof(_size)) && + splix::writeAll(fd, _data, _size) && + splix::writeAll(fd, &_checksum, sizeof(_checksum)) && + splix::writeAll(fd, &_endian, sizeof(_endian)) && + splix::writeAll(fd, &_compression, sizeof(_compression)); } BandPlane* BandPlane::restoreIntoMemory(int fd) @@ -80,14 +82,31 @@ BandPlane* BandPlane::restoreIntoMemory(int fd) BandPlane* plane; plane = new BandPlane(); - read(fd, &plane->_colorNr, sizeof(plane->_colorNr)); - read(fd, &plane->_size, sizeof(plane->_size)); - data = new unsigned char[plane->_size]; - read(fd, data, plane->_size); + if (!splix::readAll(fd, &plane->_colorNr, sizeof(plane->_colorNr)) || + !splix::readAll(fd, &plane->_size, sizeof(plane->_size))) { + delete plane; + return NULL; + } + const std::size_t trailer = sizeof(plane->_checksum) + + sizeof(plane->_endian) + sizeof(plane->_compression); + if (plane->_size > std::numeric_limits::max() - trailer || + !splix::canRead(fd, plane->_size + trailer)) { + delete plane; + return NULL; + } + data = new (std::nothrow) unsigned char[plane->_size]; + if (!data || !splix::readAll(fd, data, plane->_size)) { + delete[] data; + delete plane; + return NULL; + } plane->_data = data; - read(fd, &plane->_checksum, sizeof(plane->_checksum)); - read(fd, &plane->_endian, sizeof(plane->_endian)); - read(fd, &plane->_compression, sizeof(plane->_compression)); + if (!splix::readAll(fd, &plane->_checksum, sizeof(plane->_checksum)) || + !splix::readAll(fd, &plane->_endian, sizeof(plane->_endian)) || + !splix::readAll(fd, &plane->_compression, sizeof(plane->_compression))) { + delete plane; + return NULL; + } return plane; } diff --git a/src/cache.cpp b/src/cache.cpp index d31b57f0..94f3277f 100644 --- a/src/cache.cpp +++ b/src/cache.cpp @@ -242,7 +242,7 @@ static void* _cacheControllerThread(void *_exitVar) if (_waitingList && !(_pagesInMemory == CACHESIZE || _pagesInMemory == _pagesInTable)) { preloadPage = whatToDo; - whatToDo = ~whatToDo; + whatToDo = !whatToDo; // One of the two thing to do } else preloadPage = (_waitingList == NULL); @@ -549,6 +549,7 @@ bool CacheEntry::swapToDisk() // Swap the instance into the file if (!_page->swapToDisk(fd)) { + close(fd); unlink(_tempFile); delete[] _tempFile; _tempFile = NULL; @@ -582,6 +583,7 @@ bool CacheEntry::restoreIntoMemory() // Restore the instance if (!(_page = Page::restoreIntoMemory(fd))) { + close(fd); ERRORMSG(_("Cannot restore page into memory")); return false; } diff --git a/src/compress.cpp b/src/compress.cpp index 762f77d3..5130211a 100644 --- a/src/compress.cpp +++ b/src/compress.cpp @@ -32,472 +32,13 @@ #include "algo0x11.h" #include "algo0x13.h" #include "algo0x15.h" +#include "band_layout.h" - -/* - * Use bandwidth alignment algorithm for non-list and custom page sizes. - * This is more or less exactly how is it implemented in the ULD driver. - * - * 100 is a hardcoded margin value (50 * 2), as in the driver. - */ -unsigned long _getBandWidthAlignment(unsigned long bandwidth_align, - unsigned long xresolution, - unsigned long inputwidth) { - - const int hardMarginX = 100; - int divider = 1; - if (xresolution == 600) divider = 2; - if (xresolution == 1200) divider = 4; - - inputwidth = (inputwidth / divider - hardMarginX) * divider; - int bw_aligned = (inputwidth + bandwidth_align - 1) / bandwidth_align * bandwidth_align / 8; - - return bw_aligned; -} - -/* - * Look up bandwidths in bytes generated by Samsung ULD for 2020 series - * Samsung M2026 does not work unless it receives _exactly_ the right bandwidths - * Use the table lookup for the paper sizes hard-coded inside ULD driver, - * fallback to generic bandwidth alignment code. - */ -unsigned long _get2020BandWidthInB(unsigned char papertype, - unsigned long xresolution, - unsigned long inputwidth) { - struct Bw { - unsigned char papertype; - unsigned long width[2]; //standard and high resolution - }; - - const Bw bw[] = { - {0,640,1248}, //letter - {1,640,1248}, //legal - {2,608,1216}, //a4 - {3,544,1056}, //executive - {4,800,1600}, //ledger - {5,864,1728}, //A3 - {6,288,576}, //Env10 - {7,288,544}, //monarch envelope - {8,480,928}, //c5 envelope - {9,320,608}, //DL envelope - {10,736,1472}, //b4 - {11,544,1056}, //b5 - {12,512,992}, //b5 iso envelope - {14,288,576}, //postcard 4x6 - {16,416,832}, //a5 - {17,288,576}, //a6 - {18,384,736}, //b6 - {23,320,640}, //c6 - {24,640,1248}, //folio - {25,256,512}, //EnvPersonal - {26,288,544}, //Env9 - {27,224,416}, //IndexCard - {28,640,1248}, //Oficio - {30,416,800}, //Statement - {34,800,1568}, //8K - {35,576,1120} //16K - }; - - for (unsigned int i=0; i= 32) - return bw_aligned; - - return 0; -} - -static bool _isEmptyBand(unsigned char* band, unsigned long size) -{ - unsigned long max, mod; - - max = size / sizeof(unsigned long); - mod = size % sizeof(unsigned long); - - for (unsigned long i=0; i < max; i++) { - if (((unsigned long*)band)[i]) - return false; - } - for (unsigned long i=0; i < mod; i++) - if (band[size-i-1]) - return false; - return true; -} - -static bool _compressBandedPage(const Request& request, Page* page) -{ - unsigned long index=0, pageHeight, pageWidth, lineWidthInB, bandHeight; - unsigned long bandWidth, bandWidthInB; - unsigned long bandSize, hardMarginX, hardMarginXInB, hardMarginY; - unsigned char *planes[4], *band; - unsigned long bandNumber=0; - unsigned char colors; - - colors = page->colorsNr(); - hardMarginX = ((unsigned long)ceil(page->convertToXResolution(request. - printer()->hardMarginX())) + 7) & ~7; - hardMarginY = ceil(page->convertToYResolution(request.printer()-> - hardMarginY())); - hardMarginXInB = hardMarginX / 8; - pageWidth = page->width(); - pageHeight = page->height() - hardMarginY; - page->setWidth(pageWidth); - page->setHeight(pageHeight); - lineWidthInB = (pageWidth + 7) / 8; - bandHeight = request.printer()->bandHeight(); - if (page->xResolution() == 300 && page->yResolution() == 300) - bandHeight /= 2; - - //Patch for Samsung M2026 - if (request.printer()->specialBandWidth()) { - bandWidthInB = _get2020BandWidthInB(request.printer()->paperType(), - page->xResolution(), - pageWidth); - if (!bandWidthInB) bandWidthInB = lineWidthInB; - } else if (request.printer()->fixedBandWidth()) { - //Some of QPDL 1 and 2 models require full-width data - //regardless of the paper size. - //4960px (620 bytes) for 600 DPI - bandWidthInB = page->xResolution() * 4960 / 600 / 8; - } - else bandWidthInB = lineWidthInB; //original assumption before M2026 patch - bandWidth = bandWidthInB * 8; - bandSize = bandWidthInB * bandHeight; - - index = hardMarginY * lineWidthInB; - band = new unsigned char[bandSize]; - for (unsigned int i=0; i < colors; i++) - planes[i] = page->planeBuffer(i); - - /* - * 1. On vérifier si la bande n'est pas blanche - * |-> Si bande blanche, on passe - * '-> Sinon, on compresse - * 2. On rajoute les informations de bande (numéro de bande et de - * couleur). - * 3. On enregistre la bande dans la page. - * 4. On détruit les buffers de plans dans la page. - */ - while (pageHeight) { - unsigned long localHeight = bandHeight; - Band *current = NULL; - bool theEnd = false; - - // Special things to do for the last band - if (pageHeight < bandHeight) { - theEnd = true; - localHeight = pageHeight; - memset(band, 0, bandSize); - } - - for (unsigned int i=0; i < colors; i++) { - Algorithm *algo = NULL; - BandPlane *plane; - - switch (page->compression()) { - case 0x0D: - algo = new Algo0x0D; - break; - case 0x0E: - algo = new Algo0x0E; - break; - case 0x11: - algo = new Algo0x11; - break; - default: - ERRORMSG(_("Unknown compression algorithm. Aborted")); - return false; - } - - // Copy the data into the band depending on the algorithm options - if (algo->reverseLineColumn()) { - for (unsigned int y=0; y < localHeight; y++) { - for (unsigned int x=0; (x < lineWidthInB - hardMarginXInB) && (x < bandWidthInB); x++) { - band[x * bandHeight + y] = planes[i][index + x + hardMarginXInB + y * lineWidthInB]; // ERR? - } - for (unsigned int x=lineWidthInB - hardMarginXInB; x < bandWidthInB; x++) - band[x * bandHeight + y] = 0; - } - } else { - for (unsigned int y=0; y < localHeight; y++) { - for (unsigned int x=0; (x < lineWidthInB - hardMarginXInB) && (x < bandWidthInB); x++) { - band[x + y * bandWidthInB] = planes[i][index + x + hardMarginXInB + y * lineWidthInB]; - } - for (unsigned int x=lineWidthInB - hardMarginXInB; x < bandWidthInB; x++) - band[x + y * lineWidthInB] = 0; - } - } - - // Does the band is empty? - if (_isEmptyBand(band, bandSize)) { - delete algo; - continue; - } - - // Check if bytes have to be reversed - if (algo->inverseByte()) - for (unsigned int j=0; j < bandSize; j++) - band[j] = ~band[j]; - - // Call the compression method - plane = algo->compress(request, band, bandWidth, bandHeight); - /* - * If algorithm 0xd did not create a plane, it means that the - * complementary algorithm 0xE need to be used - */ - if (!plane && page->compression() == 0x0D) { - delete algo; - algo = new Algo0x0E; - /* Bytes has to be reversed first, as algo0xd didn't do that. */ - for (unsigned int j = 0; j < bandSize; j++) - band[j] = ~band[j]; - /* Do the encoding with algo0xe. */ - plane = algo->compress(request, band, bandWidth, bandHeight); - } - - if (plane) { - plane->setColorNr(i + 1); - if (!current) - current = new Band(bandNumber, bandWidth, bandHeight); - current->registerPlane(plane); - } - - delete algo; - } - if (current) - page->registerBand(current); - bandNumber++; - index += lineWidthInB * bandHeight; - pageHeight = theEnd ? 0 : pageHeight - bandHeight; - } - page->flushPlanes(); - delete[] band; - - return true; -} - +#include "compress_widths.inc" +#include "compress_banded.inc" #ifndef DISABLE_JBIG -static bool _compressBandedJBIGPage(const Request& request, Page* page) -{ - unsigned long index=0, pageHeight, lineWidthInB, bandHeight = 128; - unsigned long bufferWidth, specialBufferWidthInB, bandSize, hardMarginXInB=13, hardMarginY=100; - unsigned char *planes[4], *band[4]; - unsigned long bandNumber=0, xLimitInB, bufferWidthInB; - Algo0x15 *algo = new Algo0x15; - /* Image trimming are done from hardware margins defined in the ppd. */ - hardMarginXInB = ((unsigned long)ceil(page->convertToXResolution(request. - printer()->hardMarginX())) + 7) / 8; - hardMarginY = ceil(page->convertToYResolution(request.printer()-> - hardMarginY())); - bandHeight = request.printer()->bandHeight(); - lineWidthInB = ((page->width()) + 7) / 8; - // Compute an updated page height, clipped on top. - pageHeight = page->height() - hardMarginY; - // Compute the buffer width that is nearest multiple of 256 to the original. - bufferWidth = page->width() & ~255; - if ((bufferWidth + 128) < page->width()) - bufferWidth += 256; - - if (request.printer()->specialBandWidth()) { - specialBufferWidthInB = _get2020BandWidthInB(request.printer()->paperType(), - page->xResolution(), - page->width()); - if (specialBufferWidthInB) bufferWidth = specialBufferWidthInB * 8; - } - // Update the page heigth. - page->setHeight(pageHeight); - // Update the page width. - page->setWidth(bufferWidth); - bufferWidthInB = (bufferWidth + 7) / 8; - bandSize = bufferWidthInB * bandHeight; - index = hardMarginY * lineWidthInB; - for (unsigned int i=0; i < page->colorsNr(); i++) { - band[i] = new unsigned char[bandSize]; - planes[i] = page->planeBuffer(i); - } - /* - Here, limit the width of the copied image to the buffer, as the buffer - width varies and can lead to 6 practical cases, the following is a - condensed code. - */ - if (hardMarginXInB + bufferWidthInB < lineWidthInB - hardMarginXInB) - xLimitInB = hardMarginXInB + bufferWidthInB; - else - xLimitInB = lineWidthInB - hardMarginXInB; - bool theEnd = false; - unsigned long indexSizeIncrement = bandHeight * lineWidthInB; - unsigned long localHeight = bandHeight; - while (pageHeight) { - Band *current = NULL; - bool cmyPlanesHasData = false; - // Special things to do for the last band - if (pageHeight < bandHeight) { - theEnd = true; - localHeight = pageHeight; - for (unsigned int i=0; i < page->colorsNr(); i++) - memset(band[i], 0, bandSize); - } - for (unsigned int i=0; i < page->colorsNr(); i++) { - for (unsigned int y=0; y < localHeight; y++) { - for (unsigned int x=hardMarginXInB; x < xLimitInB; x++) - band[i][x - hardMarginXInB + y * bufferWidthInB] = - planes[i][index + x + y * lineWidthInB]; - for (unsigned int x=lineWidthInB - 2 * hardMarginXInB; - x < bufferWidthInB; x++) - band[i][x + y * bufferWidthInB] = 0; - } - } - // Are the CMY planes completely empty in the band? - for (unsigned int i=0; i < page->colorsNr() - 1; i++) - if (!_isEmptyBand(band[i], bandSize)) { - cmyPlanesHasData = true; - break; - }; - // Compress the entire band. - if (cmyPlanesHasData) { - for (unsigned int i=0; i < page->colorsNr(); i++) { - BandPlane *plane = algo->compress(request, band[i], - bufferWidth, bandHeight); - if (plane) { - plane->setColorNr((1 == page->colorsNr()) ? 4:i + 1); - if (!current) - current = new Band(bandNumber, bufferWidth, bandHeight); - current->registerPlane(plane); - } - } - } else if (!_isEmptyBand(band[page->colorsNr() - 1], bandSize)) { - // Compress only the K band. - BandPlane *plane = algo->compress(request, - band[page->colorsNr() - 1], - bufferWidth, bandHeight); - if (plane) { - plane->setColorNr(4); - if (!current) - current = new Band(bandNumber, bufferWidth, bandHeight); - current->registerPlane(plane); - } - } - if (current) - page->registerBand(current); - bandNumber++; - index += indexSizeIncrement; - pageHeight = theEnd ? 0 : pageHeight - bandHeight; - } - if (page->bandsNr() > 0) - page->setBIH(algo->getBIHdata()); - page->flushPlanes(); - for (unsigned int i=0; i < page->colorsNr(); i++) - delete[] band[i]; - delete algo; - return true; -} - -static bool _compressWholePage(const Request& request, Page* page) -{ - unsigned long hardMarginX, hardMarginXInB, hardMarginY, lineWidthInB; - unsigned long pageWidth, bandHeight, planeHeight, pageHeight, index; - unsigned long bandNumber=0; - unsigned char *buffer; - Band *current = NULL; - Algo0x13 algo[4]; - - hardMarginX = ((unsigned long)ceil(page->convertToXResolution(request. - printer()->hardMarginX())) + 7) & ~7; - hardMarginY = ceil(page->convertToYResolution(request.printer()-> - hardMarginY())); - hardMarginXInB = hardMarginX / 8; - pageWidth = page->width() - hardMarginX * 2; - pageHeight = page->height() - hardMarginY * 2; - page->setWidth(pageWidth); - page->setHeight(pageHeight); - lineWidthInB = (pageWidth + 7) / 8; - bandHeight = request.printer()->bandHeight(); - // Alignment of the page height on band height - planeHeight = ((pageHeight + bandHeight - 1) / bandHeight) * bandHeight; - buffer = new unsigned char[lineWidthInB * planeHeight]; - - do { - current = NULL; - for (unsigned int i=0; i < page->colorsNr(); i++) { - unsigned char *curPlane = page->planeBuffer(i); - BandPlane *plane; - - index = hardMarginY * (lineWidthInB + 2 * hardMarginXInB) + - hardMarginXInB; - for (unsigned int y=0; y < pageHeight; y++, - index += 2 * hardMarginXInB) { - for (unsigned int x=0; x < lineWidthInB; x++, index++) { - buffer[x + y * lineWidthInB] = curPlane[index]; - } - } - memset(buffer + pageHeight * lineWidthInB, 0, - (planeHeight - pageHeight) * lineWidthInB); - - // Call the compression method - plane = algo[i].compress(request, buffer, page->width(), - planeHeight); - if (plane) { - plane->setColorNr(i + 1); - if (!current) - current = new Band(bandNumber, page->width(), - request.printer()->bandHeight()); - current->registerPlane(plane); - } - } - if (current) - page->registerBand(current); - bandNumber++; - } while (current); - - page->flushPlanes(); - delete[] buffer; - - return true; -} -#endif /* DISABLE_JBIG */ - -bool compressPage(const Request& request, Page* page) -{ - switch(page->compression()) { - case 0x0D: - case 0x0E: - case 0x11: - return _compressBandedPage(request, page); - case 0x13: -#ifndef DISABLE_JBIG - return _compressWholePage(request, page); -#else - ERRORMSG(_("J-BIG compression algorithm has been disabled during " - "the compilation. Please recompile SpliX and enable the " - "J-BIG compression algorithm.")); - break; -#endif /* DISABLE_JBIG */ - case 0x15: -#ifndef DISABLE_JBIG - return _compressBandedJBIGPage(request, page); -#else - ERRORMSG(_("J-BIG compression algorithm has been disabled during " - "the compilation. Please recompile SpliX and enable the " - "J-BIG compression algorithm.")); - break; -#endif /* DISABLE_JBIG */ - default: - ERRORMSG(_("Compression algorithm 0x%lX does not exist"), - page->compression()); - } - return false; -} +#include "compress_jbig.inc" +#endif +#include "compress_dispatch.inc" /* vim: set expandtab tabstop=4 shiftwidth=4 smarttab tw=80 cin enc=utf8: */ - diff --git a/src/compress_banded.inc b/src/compress_banded.inc new file mode 100644 index 00000000..b370cf89 --- /dev/null +++ b/src/compress_banded.inc @@ -0,0 +1,143 @@ +/* + * Part of the SpliX compression implementation. + * See compress.cpp for copyright and license terms. + */ +static bool _compressBandedPage(const Request& request, Page* page) +{ + unsigned long index=0, pageHeight, pageWidth, lineWidthInB, bandHeight; + unsigned long bandWidth, bandWidthInB; + unsigned long bandSize, hardMarginX, hardMarginXInB, hardMarginY; + unsigned char *planes[4], *band; + unsigned long bandNumber=0; + unsigned char colors; + + colors = page->colorsNr(); + hardMarginX = ((unsigned long)ceil(page->convertToXResolution(request. + printer()->hardMarginX())) + 7) & ~7; + hardMarginY = ceil(page->convertToYResolution(request.printer()-> + hardMarginY())); + hardMarginXInB = hardMarginX / 8; + pageWidth = page->width(); + pageHeight = page->height() - hardMarginY; + page->setWidth(pageWidth); + page->setHeight(pageHeight); + lineWidthInB = (pageWidth + 7) / 8; + bandHeight = request.printer()->bandHeight(); + if (page->xResolution() == 300 && page->yResolution() == 300) + bandHeight /= 2; + + //Patch for Samsung M2026 + if (request.printer()->specialBandWidth()) { + bandWidthInB = _get2020BandWidthInB(request.printer()->paperType(), + page->xResolution(), + pageWidth); + if (!bandWidthInB) bandWidthInB = lineWidthInB; + } else if (request.printer()->fixedBandWidth()) { + //Some of QPDL 1 and 2 models require full-width data + //regardless of the paper size. + //4960px (620 bytes) for 600 DPI + bandWidthInB = splix::fixedBandWidthBytes(page->xResolution()); + } + else bandWidthInB = lineWidthInB; //original assumption before M2026 patch + bandWidth = bandWidthInB * 8; + bandSize = bandWidthInB * bandHeight; + + index = hardMarginY * lineWidthInB; + band = new unsigned char[bandSize]; + for (unsigned int i=0; i < colors; i++) + planes[i] = page->planeBuffer(i); + + /* + * 1. On vérifier si la bande n'est pas blanche + * |-> Si bande blanche, on passe + * '-> Sinon, on compresse + * 2. On rajoute les informations de bande (numéro de bande et de + * couleur). + * 3. On enregistre la bande dans la page. + * 4. On détruit les buffers de plans dans la page. + */ + while (pageHeight) { + unsigned long localHeight = bandHeight; + Band *current = NULL; + bool theEnd = false; + + // Special things to do for the last band + if (pageHeight < bandHeight) { + theEnd = true; + localHeight = pageHeight; + memset(band, 0, bandSize); + } + + for (unsigned int i=0; i < colors; i++) { + Algorithm *algo = NULL; + BandPlane *plane; + + switch (page->compression()) { + case 0x0D: + algo = new Algo0x0D; + break; + case 0x0E: + algo = new Algo0x0E; + break; + case 0x11: + algo = new Algo0x11; + break; + default: + ERRORMSG(_("Unknown compression algorithm. Aborted")); + return false; + } + + const splix::BandLayout layout = algo->reverseLineColumn() + ? splix::ColumnMajor + : splix::RowMajor; + splix::copyBandRows(band, bandWidthInB, bandHeight, + planes[i] + index, lineWidthInB, + hardMarginXInB, localHeight, layout); + + // Does the band is empty? + if (_isEmptyBand(band, bandSize)) { + delete algo; + continue; + } + + // Check if bytes have to be reversed + if (algo->inverseByte()) + for (unsigned int j=0; j < bandSize; j++) + band[j] = ~band[j]; + + // Call the compression method + plane = algo->compress(request, band, bandWidth, bandHeight); + /* + * If algorithm 0xd did not create a plane, it means that the + * complementary algorithm 0xE need to be used + */ + if (!plane && page->compression() == 0x0D) { + delete algo; + algo = new Algo0x0E; + /* Bytes has to be reversed first, as algo0xd didn't do that. */ + for (unsigned int j = 0; j < bandSize; j++) + band[j] = ~band[j]; + /* Do the encoding with algo0xe. */ + plane = algo->compress(request, band, bandWidth, bandHeight); + } + + if (plane) { + plane->setColorNr(i + 1); + if (!current) + current = new Band(bandNumber, bandWidth, bandHeight); + current->registerPlane(plane); + } + + delete algo; + } + if (current) + page->registerBand(current); + bandNumber++; + index += lineWidthInB * bandHeight; + pageHeight = theEnd ? 0 : pageHeight - bandHeight; + } + page->flushPlanes(); + delete[] band; + + return true; +} diff --git a/src/compress_dispatch.inc b/src/compress_dispatch.inc new file mode 100644 index 00000000..96e2e828 --- /dev/null +++ b/src/compress_dispatch.inc @@ -0,0 +1,35 @@ +/* + * Part of the SpliX compression implementation. + * See compress.cpp for copyright and license terms. + */ +bool compressPage(const Request& request, Page* page) +{ + switch(page->compression()) { + case 0x0D: + case 0x0E: + case 0x11: + return _compressBandedPage(request, page); + case 0x13: +#ifndef DISABLE_JBIG + return _compressWholePage(request, page); +#else + ERRORMSG(_("J-BIG compression algorithm has been disabled during " + "the compilation. Please recompile SpliX and enable the " + "J-BIG compression algorithm.")); + break; +#endif /* DISABLE_JBIG */ + case 0x15: +#ifndef DISABLE_JBIG + return _compressBandedJBIGPage(request, page); +#else + ERRORMSG(_("J-BIG compression algorithm has been disabled during " + "the compilation. Please recompile SpliX and enable the " + "J-BIG compression algorithm.")); + break; +#endif /* DISABLE_JBIG */ + default: + ERRORMSG(_("Compression algorithm 0x%lX does not exist"), + page->compression()); + } + return false; +} diff --git a/src/compress_jbig.inc b/src/compress_jbig.inc new file mode 100644 index 00000000..22af543f --- /dev/null +++ b/src/compress_jbig.inc @@ -0,0 +1,182 @@ +/* + * Part of the SpliX compression implementation. + * See compress.cpp for copyright and license terms. + */ +static bool _compressBandedJBIGPage(const Request& request, Page* page) +{ + unsigned long index=0, pageHeight, lineWidthInB, bandHeight = 128; + unsigned long bufferWidth, specialBufferWidthInB, bandSize, hardMarginXInB=13, hardMarginY=100; + unsigned char *planes[4], *band[4]; + unsigned long bandNumber=0, xLimitInB, bufferWidthInB; + Algo0x15 *algo = new Algo0x15; + /* Image trimming are done from hardware margins defined in the ppd. */ + hardMarginXInB = ((unsigned long)ceil(page->convertToXResolution(request. + printer()->hardMarginX())) + 7) / 8; + hardMarginY = ceil(page->convertToYResolution(request.printer()-> + hardMarginY())); + bandHeight = request.printer()->bandHeight(); + lineWidthInB = ((page->width()) + 7) / 8; + // Compute an updated page height, clipped on top. + pageHeight = page->height() - hardMarginY; + // Compute the buffer width that is nearest multiple of 256 to the original. + bufferWidth = page->width() & ~255; + if ((bufferWidth + 128) < page->width()) + bufferWidth += 256; + + if (request.printer()->specialBandWidth()) { + specialBufferWidthInB = _get2020BandWidthInB(request.printer()->paperType(), + page->xResolution(), + page->width()); + if (specialBufferWidthInB) bufferWidth = specialBufferWidthInB * 8; + } + // Update the page heigth. + page->setHeight(pageHeight); + // Update the page width. + page->setWidth(bufferWidth); + bufferWidthInB = (bufferWidth + 7) / 8; + bandSize = bufferWidthInB * bandHeight; + index = hardMarginY * lineWidthInB; + for (unsigned int i=0; i < page->colorsNr(); i++) { + band[i] = new unsigned char[bandSize]; + planes[i] = page->planeBuffer(i); + } + /* + Here, limit the width of the copied image to the buffer, as the buffer + width varies and can lead to 6 practical cases, the following is a + condensed code. + */ + if (hardMarginXInB + bufferWidthInB < lineWidthInB - hardMarginXInB) + xLimitInB = hardMarginXInB + bufferWidthInB; + else + xLimitInB = lineWidthInB - hardMarginXInB; + bool theEnd = false; + unsigned long indexSizeIncrement = bandHeight * lineWidthInB; + unsigned long localHeight = bandHeight; + while (pageHeight) { + Band *current = NULL; + bool cmyPlanesHasData = false; + // Special things to do for the last band + if (pageHeight < bandHeight) { + theEnd = true; + localHeight = pageHeight; + for (unsigned int i=0; i < page->colorsNr(); i++) + memset(band[i], 0, bandSize); + } + for (unsigned int i=0; i < page->colorsNr(); i++) { + for (unsigned int y=0; y < localHeight; y++) { + for (unsigned int x=hardMarginXInB; x < xLimitInB; x++) + band[i][x - hardMarginXInB + y * bufferWidthInB] = + planes[i][index + x + y * lineWidthInB]; + for (unsigned int x=lineWidthInB - 2 * hardMarginXInB; + x < bufferWidthInB; x++) + band[i][x + y * bufferWidthInB] = 0; + } + } + // Are the CMY planes completely empty in the band? + for (unsigned int i=0; + i + 1 < static_cast(page->colorsNr()); i++) + if (!_isEmptyBand(band[i], bandSize)) { + cmyPlanesHasData = true; + break; + }; + // Compress the entire band. + if (cmyPlanesHasData) { + for (unsigned int i=0; i < page->colorsNr(); i++) { + BandPlane *plane = algo->compress(request, band[i], + bufferWidth, bandHeight); + if (plane) { + plane->setColorNr((1 == page->colorsNr()) ? 4:i + 1); + if (!current) + current = new Band(bandNumber, bufferWidth, bandHeight); + current->registerPlane(plane); + } + } + } else if (!_isEmptyBand(band[page->colorsNr() - 1], bandSize)) { + // Compress only the K band. + BandPlane *plane = algo->compress(request, + band[page->colorsNr() - 1], + bufferWidth, bandHeight); + if (plane) { + plane->setColorNr(4); + if (!current) + current = new Band(bandNumber, bufferWidth, bandHeight); + current->registerPlane(plane); + } + } + if (current) + page->registerBand(current); + bandNumber++; + index += indexSizeIncrement; + pageHeight = theEnd ? 0 : pageHeight - bandHeight; + } + if (page->bandsNr() > 0) + page->setBIH(algo->getBIHdata()); + page->flushPlanes(); + for (unsigned int i=0; i < page->colorsNr(); i++) + delete[] band[i]; + delete algo; + return true; +} + +static bool _compressWholePage(const Request& request, Page* page) +{ + unsigned long hardMarginX, hardMarginXInB, hardMarginY, lineWidthInB; + unsigned long pageWidth, bandHeight, planeHeight, pageHeight, index; + unsigned long bandNumber=0; + unsigned char *buffer; + Band *current = NULL; + Algo0x13 algo[4]; + + hardMarginX = ((unsigned long)ceil(page->convertToXResolution(request. + printer()->hardMarginX())) + 7) & ~7; + hardMarginY = ceil(page->convertToYResolution(request.printer()-> + hardMarginY())); + hardMarginXInB = hardMarginX / 8; + pageWidth = page->width() - hardMarginX * 2; + pageHeight = page->height() - hardMarginY * 2; + page->setWidth(pageWidth); + page->setHeight(pageHeight); + lineWidthInB = (pageWidth + 7) / 8; + bandHeight = request.printer()->bandHeight(); + // Alignment of the page height on band height + planeHeight = ((pageHeight + bandHeight - 1) / bandHeight) * bandHeight; + buffer = new unsigned char[lineWidthInB * planeHeight]; + + do { + current = NULL; + for (unsigned int i=0; i < page->colorsNr(); i++) { + unsigned char *curPlane = page->planeBuffer(i); + BandPlane *plane; + + index = hardMarginY * (lineWidthInB + 2 * hardMarginXInB) + + hardMarginXInB; + for (unsigned int y=0; y < pageHeight; y++, + index += 2 * hardMarginXInB) { + for (unsigned int x=0; x < lineWidthInB; x++, index++) { + buffer[x + y * lineWidthInB] = curPlane[index]; + } + } + memset(buffer + pageHeight * lineWidthInB, 0, + (planeHeight - pageHeight) * lineWidthInB); + + // Call the compression method + plane = algo[i].compress(request, buffer, page->width(), + planeHeight); + if (plane) { + plane->setColorNr(i + 1); + if (!current) + current = new Band(bandNumber, page->width(), + request.printer()->bandHeight()); + current->registerPlane(plane); + } + } + if (current) + page->registerBand(current); + bandNumber++; + } while (current); + + page->flushPlanes(); + delete[] buffer; + + return true; +} diff --git a/src/compress_widths.inc b/src/compress_widths.inc new file mode 100644 index 00000000..4939a97e --- /dev/null +++ b/src/compress_widths.inc @@ -0,0 +1,104 @@ +/* + * Part of the SpliX compression implementation. + * See compress.cpp for copyright and license terms. + */ +/* + * Use bandwidth alignment algorithm for non-list and custom page sizes. + * This is more or less exactly how is it implemented in the ULD driver. + * + * 100 is a hardcoded margin value (50 * 2), as in the driver. + */ +unsigned long _getBandWidthAlignment(unsigned long bandwidth_align, + unsigned long xresolution, + unsigned long inputwidth) { + + const int hardMarginX = 100; + int divider = 1; + if (xresolution == 600) divider = 2; + if (xresolution == 1200) divider = 4; + + inputwidth = (inputwidth / divider - hardMarginX) * divider; + int bw_aligned = (inputwidth + bandwidth_align - 1) / bandwidth_align * bandwidth_align / 8; + + return bw_aligned; +} + +/* + * Look up bandwidths in bytes generated by Samsung ULD for 2020 series + * Samsung M2026 does not work unless it receives _exactly_ the right bandwidths + * Use the table lookup for the paper sizes hard-coded inside ULD driver, + * fallback to generic bandwidth alignment code. + */ +unsigned long _get2020BandWidthInB(unsigned char papertype, + unsigned long xresolution, + unsigned long inputwidth) { + struct Bw { + unsigned char papertype; + unsigned long width[2]; //standard and high resolution + }; + + const Bw bw[] = { + {0,640,1248}, //letter + {1,640,1248}, //legal + {2,608,1216}, //a4 + {3,544,1056}, //executive + {4,800,1600}, //ledger + {5,864,1728}, //A3 + {6,288,576}, //Env10 + {7,288,544}, //monarch envelope + {8,480,928}, //c5 envelope + {9,320,608}, //DL envelope + {10,736,1472}, //b4 + {11,544,1056}, //b5 + {12,512,992}, //b5 iso envelope + {14,288,576}, //postcard 4x6 + {16,416,832}, //a5 + {17,288,576}, //a6 + {18,384,736}, //b6 + {23,320,640}, //c6 + {24,640,1248}, //folio + {25,256,512}, //EnvPersonal + {26,288,544}, //Env9 + {27,224,416}, //IndexCard + {28,640,1248}, //Oficio + {30,416,800}, //Statement + {34,800,1568}, //8K + {35,576,1120} //16K + }; + + for (unsigned int i=0; i= 32) + return bw_aligned; + + return 0; +} + +static bool _isEmptyBand(unsigned char* band, unsigned long size) +{ + unsigned long max, mod; + + max = size / sizeof(unsigned long); + mod = size % sizeof(unsigned long); + + for (unsigned long i=0; i < max; i++) { + if (((unsigned long*)band)[i]) + return false; + } + for (unsigned long i=0; i < mod; i++) + if (band[size-i-1]) + return false; + return true; +} diff --git a/src/document.cpp b/src/document.cpp index d5e3b771..3c9821e1 100644 --- a/src/document.cpp +++ b/src/document.cpp @@ -66,7 +66,7 @@ bool Document::load(const Request& request) */ Page* Document::getNextRawPage(const Request& request) { - cups_page_header_t header; + cups_page_header2_t header; unsigned long pageWidth, pageWidthInB, pageHeight, clippingX=0, clippingY=0; unsigned long documentWidth, documentHeight, lineSize, planeSize, index=0; unsigned long bytesToCopy, marginWidthInB=0, marginHeight=0; @@ -81,7 +81,7 @@ Page* Document::getNextRawPage(const Request& request) ERRORMSG(_("The raster hasn't been loaded")); return NULL; } - if (!cupsRasterReadHeader(_raster, &header) || !header.cupsBytesPerLine || + if (!cupsRasterReadHeader2(_raster, &header) || !header.cupsBytesPerLine || !header.PageSize[1]) { DEBUGMSG(_("No more pages")); _lastPage = true; diff --git a/src/io_utils.cpp b/src/io_utils.cpp new file mode 100644 index 00000000..dfffa008 --- /dev/null +++ b/src/io_utils.cpp @@ -0,0 +1,49 @@ +/* Checked POSIX I/O helpers. Copyright (C) 2026 Michael Maertzdorf. */ +#include "io_utils.h" + +#include +#include +#include + +namespace splix { + +bool readAll(int fd, void* buffer, std::size_t size) +{ + unsigned char* next = static_cast(buffer); + while (size != 0) { + const ssize_t count = read(fd, next, size); + if (count < 0 && errno == EINTR) + continue; + if (count <= 0) + return false; + next += count; + size -= static_cast(count); + } + return true; +} + +bool writeAll(int fd, const void* buffer, std::size_t size) +{ + const unsigned char* next = static_cast(buffer); + while (size != 0) { + const ssize_t count = write(fd, next, size); + if (count < 0 && errno == EINTR) + continue; + if (count <= 0) + return false; + next += count; + size -= static_cast(count); + } + return true; +} + +bool canRead(int fd, std::size_t size) +{ + struct stat info; + const off_t position = lseek(fd, 0, SEEK_CUR); + if (position < 0 || fstat(fd, &info) != 0 || info.st_size < position) + return false; + return size <= static_cast(info.st_size - position); +} + +} // namespace splix diff --git a/src/module.mk b/src/module.mk index 8c02c60f..7c1ad4a8 100644 --- a/src/module.mk +++ b/src/module.mk @@ -5,6 +5,7 @@ rastertoqpdl_SRC += src/rastertoqpdl.cpp src/request.cpp \ src/printer.cpp src/qpdl.cpp src/document.cpp \ src/core.cpp src/compress.cpp src/algorithm.cpp \ + src/band_layout.cpp src/io_utils.cpp \ src/ppdfile.cpp src/page.cpp src/colors.cpp \ src/band.cpp src/bandplane.cpp src/cache.cpp \ src/rendering.cpp src/semaphore.cpp \ diff --git a/src/page.cpp b/src/page.cpp index 3efb4f65..0a6a3e88 100644 --- a/src/page.cpp +++ b/src/page.cpp @@ -23,6 +23,7 @@ #include #include "band.h" #include "errlog.h" +#include "io_utils.h" /* * This magic formula reverse the bit of a byte. ie. the bit 1 becomes the @@ -117,77 +118,7 @@ void Page::flushPlanes() -/* - * Mise sur disque / Rechargement - * Swapping / restoring - */ -bool Page::swapToDisk(int fd) -{ - unsigned long i; - Band* band; - - if (_planes[0] || _planes[1] || _planes[2] || _planes[3]) { - ERRORMSG(_("Cannot swap page instance which still contains bitmap " - "representation")); - return false; - } - write(fd, &_xResolution, sizeof(_xResolution)); - write(fd, &_yResolution, sizeof(_yResolution)); - write(fd, &_width, sizeof(_width)); - write(fd, &_height, sizeof(_height)); - write(fd, &_colors, sizeof(_colors)); - write(fd, &_pageNr, sizeof(_pageNr)); - write(fd, &_copiesNr, sizeof(_copiesNr)); - write(fd, &_compression, sizeof(_compression)); - write(fd, &_empty, sizeof(_empty)); - write(fd, &_bandsNr, sizeof(_bandsNr)); - /* Carefully check if there is BIH data and compression type is 0x15, - before saving BIH data to file. */ - if (( 0x15 == _compression ) && ( _bandsNr > 0 ) && ( NULL != _bih )) - write(fd, _bih, 20); - for (i=0, band = _firstBand; i < _bandsNr; i++) { - if (!band->swapToDisk(fd)) - return false; - band = band->sibling(); - } - - return true; -} - -Page* Page::restoreIntoMemory(int fd) -{ - unsigned long nr; - Page* page; - - page = new Page(); - read(fd, &page->_xResolution, sizeof(page->_xResolution)); - read(fd, &page->_yResolution, sizeof(page->_yResolution)); - read(fd, &page->_width, sizeof(page->_width)); - read(fd, &page->_height, sizeof(page->_height)); - read(fd, &page->_colors, sizeof(page->_colors)); - read(fd, &page->_pageNr, sizeof(page->_pageNr)); - read(fd, &page->_copiesNr, sizeof(page->_copiesNr)); - read(fd, &page->_compression, sizeof(page->_compression)); - read(fd, &page->_empty, sizeof(page->_empty)); - read(fd, &nr, sizeof(nr)); - /* Check if compression type is 0x15 and that there is at least one - image band before reading BIH data. */ - if (( 0x15 == page->_compression ) && ( nr > 0 )) { - unsigned char bih[20]; - read(fd, bih, 20); - page->setBIH(bih); - } - for (unsigned int i=0; i < nr; i++) { - Band *band = Band::restoreIntoMemory(fd); - if (!band) { - delete page; - return NULL; - } - page->registerBand(band); - } - - return page; -} +#include "page_io.inc" void Page::setBIH(const unsigned char *bih_data) { if (NULL == _bih) diff --git a/src/page_io.inc b/src/page_io.inc new file mode 100644 index 00000000..f2a1309c --- /dev/null +++ b/src/page_io.inc @@ -0,0 +1,69 @@ +/* Page swap-file serialization. See page.cpp for license terms. */ +bool Page::swapToDisk(int fd) +{ + unsigned long i; + Band* band; + + if (_planes[0] || _planes[1] || _planes[2] || _planes[3]) { + ERRORMSG(_("Cannot swap page instance which still contains bitmap " + "representation")); + return false; + } + if (!splix::writeAll(fd, &_xResolution, sizeof(_xResolution)) || + !splix::writeAll(fd, &_yResolution, sizeof(_yResolution)) || + !splix::writeAll(fd, &_width, sizeof(_width)) || + !splix::writeAll(fd, &_height, sizeof(_height)) || + !splix::writeAll(fd, &_colors, sizeof(_colors)) || + !splix::writeAll(fd, &_pageNr, sizeof(_pageNr)) || + !splix::writeAll(fd, &_copiesNr, sizeof(_copiesNr)) || + !splix::writeAll(fd, &_compression, sizeof(_compression)) || + !splix::writeAll(fd, &_empty, sizeof(_empty)) || + !splix::writeAll(fd, &_bandsNr, sizeof(_bandsNr))) + return false; + if (0x15 == _compression && _bandsNr > 0 && _bih && + !splix::writeAll(fd, _bih, 20)) + return false; + for (i=0, band = _firstBand; i < _bandsNr; i++) { + if (!band->swapToDisk(fd)) + return false; + band = band->sibling(); + } + return true; +} + +Page* Page::restoreIntoMemory(int fd) +{ + unsigned long nr; + Page* page = new Page(); + if (!splix::readAll(fd, &page->_xResolution, sizeof(page->_xResolution)) || + !splix::readAll(fd, &page->_yResolution, sizeof(page->_yResolution)) || + !splix::readAll(fd, &page->_width, sizeof(page->_width)) || + !splix::readAll(fd, &page->_height, sizeof(page->_height)) || + !splix::readAll(fd, &page->_colors, sizeof(page->_colors)) || + page->_colors > 4 || + !splix::readAll(fd, &page->_pageNr, sizeof(page->_pageNr)) || + !splix::readAll(fd, &page->_copiesNr, sizeof(page->_copiesNr)) || + !splix::readAll(fd, &page->_compression, sizeof(page->_compression)) || + !splix::readAll(fd, &page->_empty, sizeof(page->_empty)) || + !splix::readAll(fd, &nr, sizeof(nr))) { + delete page; + return NULL; + } + if (0x15 == page->_compression && nr > 0) { + unsigned char bih[20]; + if (!splix::readAll(fd, bih, 20)) { + delete page; + return NULL; + } + page->setBIH(bih); + } + for (unsigned int i=0; i < nr; i++) { + Band *band = Band::restoreIntoMemory(fd); + if (!band) { + delete page; + return NULL; + } + page->registerBand(band); + } + return page; +} diff --git a/src/pstoqpdl.cpp b/src/pstoqpdl.cpp index 3f86ab8c..62545706 100644 --- a/src/pstoqpdl.cpp +++ b/src/pstoqpdl.cpp @@ -180,9 +180,8 @@ static char *_readCMSFile(PPDFile& ppd, const char *manufacturer, bool csa) */ int main(int argc, char **argv) { - const char *jobid, *user, *title, *options, *ppdFile, *file; + const char *options, *ppdFile, *file; const char *paperType, *manufacturer; - unsigned long copies; bool pageSetup=false; char buffer[1024]; char *crd, *csa; @@ -195,12 +194,8 @@ int main(int argc, char **argv) "[file]\n"), argv[0]); return 1; } - jobid = argv[1]; - user = argv[2]; - title = argv[3]; options = argv[5]; file = argc == 7 ? argv[6] : NULL; - copies = strtol(argv[4], (char **)NULL, 10); ppdFile = getenv("PPD"); // Get more information on the SpliX environment (for debugging) @@ -241,10 +236,8 @@ int main(int argc, char **argv) delete[] csa; csa = NULL; } - while (!(feof(stdin))) { - fgets((char *)&buffer, sizeof(buffer), stdin); - fprintf(stdout, "%s", (char *)&buffer); - } + while (fgets((char *)&buffer, sizeof(buffer), stdin)) + fprintf(stdout, "%s", (char *)&buffer); } else { // Insert the MediaChoice and colour correction information into diff --git a/src/qpdl.cpp b/src/qpdl.cpp index 035432f5..31627c6e 100644 --- a/src/qpdl.cpp +++ b/src/qpdl.cpp @@ -247,10 +247,13 @@ static bool _renderBand(const Request& request, const Band* band, bool mono) switch (plane->endian()) { case BandPlane::Dependant: - *(uint32_t*)(&header + size) = (uint32_t)plane-> - dataSize(); - *(uint32_t*)(&header + size + 4) = (uint32_t)state; + { + const uint32_t planeSize = (uint32_t)plane->dataSize(); + memcpy(header + size, &planeSize, sizeof(planeSize)); + memcpy(header + size + sizeof(planeSize), &state, + sizeof(state)); break; + } case BandPlane::BigEndian: // Data size 24 - 31 header[size+0] = plane->dataSize() >> 24; diff --git a/tests/Dockerfile b/tests/Dockerfile new file mode 100644 index 00000000..22cf995c --- /dev/null +++ b/tests/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:stable-slim + +RUN sed -i 's/^Types: deb$/Types: deb deb-src/' \ + /etc/apt/sources.list.d/debian.sources \ + && apt-get update -qq \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \ + build-essential \ + cups-ppdc \ + dpkg-dev \ + libcups2-dev \ + pkg-config \ + python3 \ + && mkdir -p /tmp/jbig /opt/jbig \ + && cd /tmp/jbig \ + && apt-get source -qq jbigkit \ + && make -s -C jbigkit-*/libjbig libjbig85.a \ + && cp jbigkit-*/libjbig/libjbig85.a /opt/jbig/ \ + && rm -rf /tmp/jbig /var/lib/apt/lists/* + +ENV LIBRARY_PATH=/opt/jbig +WORKDIR /src diff --git a/tests/fixed_bandwidth_ppd_test.py b/tests/fixed_bandwidth_ppd_test.py new file mode 100755 index 00000000..7a7be643 --- /dev/null +++ b/tests/fixed_bandwidth_ppd_test.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Characterize generated PPD support for fixed-bandwidth printers.""" + +from pathlib import Path +import sys + + +FIXED_MODELS = ( + "ml1510", + "ml1520", + "ml1610", + "ml1630", + "ml1640", + "ml1710", + "ml1740", + "ml2240", + "ml2250", + "ml2251", + "ml2510", + "ph3117", +) + +VARIABLE_MODELS = ("ml1750", "ml2010", "ph3122") +ATTRIBUTE = "*QPDL FixedBandWidth: True" + + +def read_ppd(directory: Path, model: str) -> str: + path = directory / f"{model}.ppd" + if not path.is_file(): + raise AssertionError(f"missing generated PPD: {path}") + return path.read_text(encoding="latin-1") + + +def main() -> int: + directory = Path(sys.argv[1] if len(sys.argv) > 1 else "ppd") + + for model in FIXED_MODELS: + if ATTRIBUTE not in read_ppd(directory, model): + raise AssertionError(f"{model}.ppd lost fixed-bandwidth support") + + for model in VARIABLE_MODELS: + if ATTRIBUTE in read_ppd(directory, model): + raise AssertionError(f"{model}.ppd was incorrectly made fixed-width") + + print("fixed-bandwidth PPD characterization passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/fixed_bandwidth_test.cpp b/tests/fixed_bandwidth_test.cpp new file mode 100644 index 00000000..ce4b4567 --- /dev/null +++ b/tests/fixed_bandwidth_test.cpp @@ -0,0 +1,130 @@ +#include "band_layout.h" + +#include +#include +#include + +namespace { + +int failures = 0; + +template +void expectEqual(const Actual& actual, const Expected& expected, + const char* testName) +{ + if (actual == expected) + return; + + std::cerr << "FAIL: " << testName << '\n'; + ++failures; +} + +void testFixedWidths() +{ + expectEqual(splix::fixedBandWidthBytes(300), 310UL, + "300 DPI fixed width"); + expectEqual(splix::fixedBandWidthBytes(600), 620UL, + "600 DPI fixed width"); + expectEqual(splix::fixedBandWidthBytes(1200), 1240UL, + "1200 DPI fixed width"); +} + +void testRowMajorA5Padding() +{ + const std::array source = { + 0xee, 0x11, 0x12, 0x13, 0x14, + 0xef, 0x21, 0x22, 0x23, 0x24, + }; + std::array band; + band.fill(0xa5); + + splix::copyBandRows(band.data(), 6, 2, source.data(), 5, 1, 2, + splix::RowMajor); + + const std::array expected = { + 0x11, 0x12, 0x13, 0x14, 0x00, 0x00, + 0x21, 0x22, 0x23, 0x24, 0x00, 0x00, + }; + expectEqual(band, expected, "A5 rows are left-aligned and zero-padded"); +} + +void testColumnMajorA5Padding() +{ + const std::array source = { + 0xee, 0x11, 0x12, 0x13, 0x14, + 0xef, 0x21, 0x22, 0x23, 0x24, + }; + std::array band; + band.fill(0xa5); + + splix::copyBandRows(band.data(), 6, 2, source.data(), 5, 1, 2, + splix::ColumnMajor); + + const std::array expected = { + 0x11, 0x21, 0x12, 0x22, 0x13, 0x23, + 0x14, 0x24, 0x00, 0x00, 0x00, 0x00, + }; + expectEqual(band, expected, "transposed rows preserve zero padding"); +} + +void testPartialBandIsFullyInitialized() +{ + const std::array source = {0x31, 0x32, 0x41, 0x42}; + std::array band; + band.fill(0xa5); + + splix::copyBandRows(band.data(), 3, 3, source.data(), 2, 0, 2, + splix::RowMajor); + + const std::array expected = { + 0x31, 0x32, 0x00, + 0x41, 0x42, 0x00, + 0x00, 0x00, 0x00, + }; + expectEqual(band, expected, "partial bands contain no stale bytes"); +} + +void testMatchingWidthPreservesProtocolBytes() +{ + const std::array source = { + 0x51, 0x52, 0x53, 0x61, 0x62, 0x63, + }; + std::array band; + band.fill(0xa5); + + splix::copyBandRows(band.data(), 3, 2, source.data(), 3, 0, 2, + splix::RowMajor); + + expectEqual(band, source, "matching widths preserve protocol bytes"); +} + +void testOversizedMarginProducesBlankBand() +{ + const std::array source = {0x11, 0x12, 0x21, 0x22}; + std::array band; + band.fill(0xa5); + + splix::copyBandRows(band.data(), 3, 2, source.data(), 2, 2, 2, + splix::RowMajor); + + const std::array expected = {}; + expectEqual(band, expected, "oversized margins cannot underflow copy width"); +} + +} // namespace + +int main() +{ + testFixedWidths(); + testRowMajorA5Padding(); + testColumnMajorA5Padding(); + testPartialBandIsFullyInitialized(); + testMatchingWidthPreservesProtocolBytes(); + testOversizedMarginProducesBlankBand(); + + if (failures != 0) + return 1; + + std::cout << "fixed-bandwidth tests passed\n"; + return 0; +} diff --git a/tests/io_utils_test.cpp b/tests/io_utils_test.cpp new file mode 100644 index 00000000..960fe53e --- /dev/null +++ b/tests/io_utils_test.cpp @@ -0,0 +1,29 @@ +#include "io_utils.h" + +#include +#include +#include + +int main() +{ + int descriptors[2]; + if (pipe(descriptors) != 0) + return 1; + + const std::array source = {0x11, 0x22, 0x33, 0x44}; + std::array destination = {}; + const bool wrote = splix::writeAll(descriptors[1], source.data(), + source.size()); + close(descriptors[1]); + const bool read = splix::readAll(descriptors[0], destination.data(), + destination.size()); + const bool eofFails = !splix::readAll(descriptors[0], destination.data(), 1); + close(descriptors[0]); + + if (!wrote || !read || !eofFails || destination != source) { + std::cerr << "checked I/O regression failed\n"; + return 1; + } + std::cout << "checked I/O tests passed\n"; + return 0; +} diff --git a/tests/raster_fixture.cpp b/tests/raster_fixture.cpp new file mode 100644 index 00000000..7c613f5d --- /dev/null +++ b/tests/raster_fixture.cpp @@ -0,0 +1,70 @@ +#include + +#include +#include + +#include +#include +#include +#include + +namespace { + +constexpr unsigned kResolution = 600; +constexpr unsigned kPageWidthPoints = 420; +constexpr unsigned kPageHeightPoints = 595; +constexpr unsigned kRasterWidth = 3500; +constexpr unsigned kRasterHeight = 4958; + +bool writePage(cups_raster_t* raster) +{ + cups_page_header2_t header{}; + header.HWResolution[0] = kResolution; + header.HWResolution[1] = kResolution; + header.PageSize[0] = kPageWidthPoints; + header.PageSize[1] = kPageHeightPoints; + header.NumCopies = 1; + header.cupsWidth = kRasterWidth; + header.cupsHeight = kRasterHeight; + header.cupsBitsPerColor = 1; + header.cupsBitsPerPixel = 1; + header.cupsBytesPerLine = (kRasterWidth + 7) / 8; + header.cupsColorOrder = CUPS_ORDER_CHUNKED; + header.cupsColorSpace = CUPS_CSPACE_K; + header.cupsCompression = 0x11; + + if (!cupsRasterWriteHeader2(raster, &header)) + return false; + + std::vector row(header.cupsBytesPerLine, 0); + for (unsigned y = 0; y < kRasterHeight; ++y) { + std::fill(row.begin(), row.end(), 0); + if (y % 257 == 0) + row[(y / 257) % row.size()] = + static_cast(0x80U >> (y % 8)); + if (cupsRasterWritePixels(raster, row.data(), row.size()) != row.size()) + return false; + } + return true; +} + +} // namespace + +int main(int argc, char** argv) +{ + if (argc != 2) { + std::cerr << "usage: raster_fixture OUTPUT\n"; + return 2; + } + + const int fd = open(argv[1], O_CREAT | O_TRUNC | O_WRONLY, 0600); + if (fd < 0) + return 3; + + cups_raster_t* raster = cupsRasterOpen(fd, CUPS_RASTER_WRITE); + const bool ok = raster && writePage(raster); + if (raster) + cupsRasterClose(raster); + close(fd); + return ok ? 0 : 4; +} diff --git a/tests/run_docker_matrix.sh b/tests/run_docker_matrix.sh new file mode 100755 index 00000000..fb426884 --- /dev/null +++ b/tests/run_docker_matrix.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -eu + +logs=.build/test-logs +jobs=${JOBS:-2} +warnings=0 + +mkdir -p "$logs" + +run_build() { + name=$1 + shift + make distclean >/dev/null 2>&1 + if ! make DRV_ONLY=1 WARNINGS_AS_ERRORS=1 V=1 -j"$jobs" "$@" \ + >"$logs/$name.log" 2>&1; then + cat "$logs/$name.log" + return 1 + fi + cat "$logs/$name.log" + if grep -E '^[^:]+:[0-9]+:[0-9]+: (warning|error):' \ + "$logs/$name.log" >/dev/null; then + warnings=1 + fi + sh tests/run_protocol_output_test.sh optimized/rastertoqpdl +} + +run_build normal +run_build no-threads DISABLE_THREADS=1 +run_build no-jbig DISABLE_JBIG=1 +run_build sanitizers \ + "OPTIM_CXXFLAGS=-O1 -g -fsanitize=address,undefined -fno-sanitize-recover=all -fno-omit-frame-pointer" \ + "LDFLAGS=-fsanitize=address,undefined" + +sh tests/run_fixed_bandwidth_tests.sh +sh tests/run_fixed_bandwidth_ppd_test.sh +make distclean >/dev/null 2>&1 + +if [ "$warnings" -ne 0 ]; then + echo "compiler diagnostics found; inspect $logs" >&2 + exit 1 +fi + +echo "all Docker matrix checks passed" diff --git a/tests/run_fixed_bandwidth_ppd_test.sh b/tests/run_fixed_bandwidth_ppd_test.sh new file mode 100755 index 00000000..e7813eb8 --- /dev/null +++ b/tests/run_fixed_bandwidth_ppd_test.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -eu + +build_dir=${BUILD_DIR:-.build/tests} +project_dir=$(pwd) +mkdir -p "$project_dir/$build_dir" +work_dir=$(mktemp -d "$project_dir/$build_dir/fixed-bandwidth.XXXXXX") +source_dir="$work_dir/ppd" +ppd_dir="$work_dir/generated" + +trap 'rm -rf "$work_dir"' EXIT +mkdir -p "$source_dir" "$ppd_dir" + +for source in ppd/*.defs ppd/*.in ppd/compile.sh; do + tr -d '\r' < "$source" > "$source_dir/${source#ppd/}" +done + +( + cd "$source_dir" + sh ./compile.sh samsung.drv.in -I . -d "$ppd_dir" + sh ./compile.sh xerox.drv.in -I . -d "$ppd_dir" +) + +python3 tests/fixed_bandwidth_ppd_test.py "$ppd_dir" diff --git a/tests/run_fixed_bandwidth_tests.sh b/tests/run_fixed_bandwidth_tests.sh new file mode 100755 index 00000000..1ebb318b --- /dev/null +++ b/tests/run_fixed_bandwidth_tests.sh @@ -0,0 +1,49 @@ +#!/bin/sh +set -eu + +cxx=${CXX:-c++} +build_dir=${BUILD_DIR:-.build/tests} + +mkdir -p "$build_dir" + +"$cxx" \ + -std=gnu++98 \ + -Wall \ + -Wextra \ + -Werror \ + -Iinclude \ + -c src/band_layout.cpp \ + -o "$build_dir/band_layout_legacy.o" + +"$cxx" -std=gnu++98 -Wall -Wextra -Werror -Iinclude \ + -c src/io_utils.cpp -o "$build_dir/io_utils_legacy.o" + +"$cxx" \ + -std=c++23 \ + -Wall \ + -Wextra \ + -Wpedantic \ + -Werror \ + -fsanitize=address,undefined \ + -fno-sanitize-recover=all \ + -fno-omit-frame-pointer \ + -Iinclude \ + tests/fixed_bandwidth_test.cpp \ + src/band_layout.cpp \ + -o "$build_dir/fixed_bandwidth_test" + +"$build_dir/fixed_bandwidth_test" + +"$cxx" -std=c++23 -Wall -Wextra -Wpedantic -Werror \ + -fsanitize=address,undefined -fno-sanitize-recover=all \ + -fno-omit-frame-pointer -Iinclude tests/io_utils_test.cpp \ + src/io_utils.cpp -o "$build_dir/io_utils_test" +"$build_dir/io_utils_test" + +"$cxx" -std=c++23 -Wall -Wextra -Wpedantic -Werror \ + -Wno-variadic-macros \ + -fsanitize=address,undefined -fno-sanitize-recover=all \ + -fno-omit-frame-pointer -Iinclude tests/serialization_test.cpp \ + src/band.cpp src/bandplane.cpp src/io_utils.cpp \ + -o "$build_dir/serialization_test" +"$build_dir/serialization_test" diff --git a/tests/run_protocol_output_test.sh b/tests/run_protocol_output_test.sh new file mode 100644 index 00000000..f61ac1f0 --- /dev/null +++ b/tests/run_protocol_output_test.sh @@ -0,0 +1,43 @@ +#!/bin/sh +set -eu + +binary=${1:-optimized/rastertoqpdl} +build_dir=${BUILD_DIR:-.build/tests} +cxx=${CXX:-c++} + +mkdir -p "$build_dir" +work_dir=$(mktemp -d "$PWD/$build_dir/protocol.XXXXXX") +trap 'rm -rf "$work_dir"' EXIT + +"$cxx" \ + -std=c++23 \ + -Wall \ + -Wextra \ + -Wpedantic \ + -Werror \ + tests/raster_fixture.cpp \ + $(pkg-config --cflags --libs cups) \ + -o "$work_dir/raster_fixture" + +"$work_dir/raster_fixture" "$work_dir/a5.ras" + +check_model() { + model=$1 + expected=$2 + PPD="$PWD/ppd/$model.ppd" \ + timeout 30s "$binary" 1 protocol protocol 1 PageSize=A5 \ + "$work_dir/a5.ras" > "$work_dir/$model.qpdl" \ + 2> "$work_dir/$model.log" + cat "$work_dir/$model.log" + actual=$(sha256sum "$work_dir/$model.qpdl" | cut -d ' ' -f 1) + if [ "$actual" != "$expected" ]; then + echo "$model protocol hash mismatch: $actual" >&2 + exit 1 + fi + echo "$model A5 protocol output preserved ($actual)" +} + +check_model ml2010 \ + ba867847558f3aaab66396cb6905ef0a8d1f6d12468b577c3c0ad3c545298e3c +check_model ml1520 \ + efd1192e314342a6b05ef901d445fa0dceaee408ffcdbf0bc76611b5ea03da37 diff --git a/tests/serialization_test.cpp b/tests/serialization_test.cpp new file mode 100644 index 00000000..401c20ed --- /dev/null +++ b/tests/serialization_test.cpp @@ -0,0 +1,75 @@ +#include "band.h" +#include "bandplane.h" + +#include +#include +#include +#include + +namespace { + +int createTemporaryFile() +{ + char path[] = "/tmp/splix-serialization-XXXXXX"; + const int fd = mkstemp(path); + if (fd >= 0) + unlink(path); + return fd; +} + +BandPlane* createPlane() +{ + unsigned char* data = new unsigned char[4]; + data[0] = 0x11; + data[1] = 0x22; + data[2] = 0x33; + data[3] = 0x44; + BandPlane* plane = new BandPlane(); + plane->setColorNr(1); + plane->setCompression(0x11); + plane->setEndian(BandPlane::BigEndian); + plane->setData(data, 4); + return plane; +} + +bool roundTripAndRejectTruncation() +{ + const int fd = createTemporaryFile(); + if (fd < 0) + return false; + Band original(3, 4960, 128); + original.registerPlane(createPlane()); + if (!original.swapToDisk(fd) || lseek(fd, 0, SEEK_SET) < 0) { + close(fd); + return false; + } + Band* restored = Band::restoreIntoMemory(fd); + const BandPlane* plane = restored ? restored->plane(0) : NULL; + const unsigned char expected[] = {0x11, 0x22, 0x33, 0x44}; + const bool matches = restored && restored->bandNr() == 3 && + restored->width() == 4960 && restored->height() == 128 && plane && + plane->dataSize() == sizeof(expected) && + std::memcmp(plane->data(), expected, sizeof(expected)) == 0; + delete restored; + + const off_t length = lseek(fd, 0, SEEK_END); + const bool truncated = length > 0 && ftruncate(fd, length - 1) == 0 && + lseek(fd, 0, SEEK_SET) >= 0; + Band* invalid = truncated ? Band::restoreIntoMemory(fd) : NULL; + const bool rejected = truncated && invalid == NULL; + delete invalid; + close(fd); + return matches && rejected; +} + +} // namespace + +int main() +{ + if (!roundTripAndRejectTruncation()) { + std::cerr << "serialization regression failed\n"; + return 1; + } + std::cout << "serialization tests passed\n"; + return 0; +}