Six months ago, I bought a reMarkable Paper Pro. It’s a great e-ink tablet running a custom Linux distribution called reMarkable OS for note-taking and reading. The reMarkable (Pro or 2nd gen) runs Xochitl as a custom user interface on top of the Linux kernel. The reMarkable was only meant to run one UI app: Xochitl, a QT-App. Of course, as it is a Linux device, it allows us to tinker with it, use SSH and explore the file system.

Since the reMarkable OS is a regular Linux distribution this also allows users to modify the operating system and software running on their devices. Note that for some device generations, e.g. the reMarkable Paper Pro, you need to enable developer mode before directly accessing the operating system on the device.
Source: reMarkable Developer Documentation – Software Stack

The main user application on the device is called Xochitl and is proprietary, i.e. it is not open source and no source code is available. It is launched once the paper tablet starts. For devices with disk encryption enabled, it also unlocks the device. Please make sure to keep Xochitl in working order, since you will not be able to log in to your device after rebooting it otherwise. Source: reMarkable Developer Documentation – Software Stack

The fact that the software is closed source makes it harder to modify, but that’s never stopped anyone from trying. Here’s what I found when I decided to dig into the internals.

The problem: thumbnail generation

The reMarkable has this basic web interface on port 80 (http://10.11.99.1/) when connected via USB. It lets you download documents and that’s about it. There are no thumbnails, no search function and nothing fancy.

I wanted to understand how the device actually generates thumbnails. Perhaps I could extend the web interface with some missing features or generate a PDF of my document without using Python reimplementations.

Reverse Engineering Xochitl’s HTTP Router

I threw Xochitl in IDA Pro and started looking for HTTP handling code. I found the main request router pretty quickly by searching for relevant strings.

Here is the function that handles the browser UI exposed by the reMarkable:

See the raw pseudo code
void __fastcall sub_56EDC0(__int64 a1, qtwebapp::HttpRequest *this, __int64 (__fastcall **a3)())
{
  // variable declarations ...

  qtwebapp::HttpRequest::getPath(&v52, this);
  v6 = v53;
  if ( !v53 )
    v6 = (qtwebapp::HttpRequest *)&QByteArray::_empty;
  v7 = (unsigned __int8)QtPrivate::startsWith(v54, v6, 11, "/documents/");
  v8 = sub_4DBF50(&v52);
  if ( v7 )
  {
    v9 = sub_565F70(v8);
    if ( *(_BYTE *)(v9 + 16) )
    {
      v25 = *(qtwebapp::HttpRequest **)(v9 + 8);
      v52 = 2;
      v53 = 0;
      v54 = 0;
      v55 = v25;
      QMessageLogger::debug(&v46, (QMessageLogger *)&v52);
      v26 = sub_4DC040(&v46, "list request");
      qtwebapp::HttpRequest::getPath(&v49, this);
      sub_568520(v26, &v49);
      sub_4DBF50(&v49);
      QDebug::~QDebug((QDebug *)&v46);
    }
    QObject::QObject((QObject *)&v52, 0);
    v56 = a3;
    v57 = 0;
    v52 = (__int64)&off_FE1A80;
    v54 = off_FE1AF8;
    v55 = this;
    v58 = 0u;
    *(_QWORD *)&v59 = sub_5636A0;
    *((_QWORD *)&v59 + 1) = sub_563690;
    sub_45CF30(&v46, a1 + 16);
    v10 = v47;
    v11 = v48;
    v49 = 0u;
    if ( v47 )
    {
      v47 = 0;
      v48 = 0;
      v49 = v46;
    }
    v12 = v59;
    *(_QWORD *)&v59 = v10;
    *((_QWORD *)&v59 + 1) = v11;
    v13 = v49;
    v49 = v58;
    v58 = v13;
    v50 = v12;
    if ( (_QWORD)v12 )
    {
      ((void (__fastcall *)(__int128 *, __int128 *, __int64))v12)(&v49, &v49, 3);
      if ( v47 )
        v47(&v46, &v46, 3);
    }
    v57 = *(_QWORD *)(a1 + 48);
    sub_568260((qtwebapp::HttpRequest **)&v54);
    v52 = (__int64)&off_FE1A80;
    v54 = (__int64 (__fastcall **)())&unk_FE17F0;
    if ( !(_QWORD)v59 )
      goto LABEL_13;
    goto LABEL_12;
  }
  qtwebapp::HttpRequest::getPath(&v52, this);
  v14 = v53;
  if ( !v53 )
    v14 = (qtwebapp::HttpRequest *)&QByteArray::_empty;
  v15 = (unsigned __int8)QtPrivate::startsWith(v54, v14, 11, "/thumbnail/");
  v16 = sub_4DBF50(&v52);
  if ( v15 )
  {
    v17 = sub_565F70(v16);
    if ( *(_BYTE *)(v17 + 16) )
    {
      v32 = *(qtwebapp::HttpRequest **)(v17 + 8);
      v53 = 0;
      v54 = 0;
      v52 = 2;
      v55 = v32;
      QMessageLogger::debug(&v46, (QMessageLogger *)&v52);
      v33 = sub_4DC040(&v46, "thumbnail request");
      qtwebapp::HttpRequest::getPath(&v49, this);
      sub_568520(v33, &v49);
      sub_4DBF50(&v49);
      QDebug::~QDebug((QDebug *)&v46);
    }
    QObject::QObject((QObject *)&v52, 0);
    v56 = a3;
    v57 = 0;
    v52 = (__int64)&off_FE18B8;
    v54 = off_FE1930;
    v55 = this;
    v58 = 0u;
    *(_QWORD *)&v59 = sub_5636A0;
    *((_QWORD *)&v59 + 1) = sub_563690;
    sub_45CF30(&v49, a1 + 16);
    sub_B5F660((__int64)&v58, (__int64 *)&v49);
    if ( (_QWORD)v50 )
      ((void (__fastcall *)(__int128 *, __int128 *, __int64))v50)(&v49, &v49, 3);
    v57 = *(_QWORD *)(a1 + 48);
    sub_568260((qtwebapp::HttpRequest **)&v54);
    v52 = (__int64)&off_FE18B8;
    v54 = (__int64 (__fastcall **)())&unk_FE17F0;
    if ( !(_QWORD)v59 )
      goto LABEL_13;
    goto LABEL_12;
  }
  qtwebapp::HttpRequest::getPath(&v52, this);
  v18 = (unsigned __int8)sub_5684A0(&v52, "/upload");
  v19 = sub_4DBF50(&v52);
  if ( v18 )
  {
    v20 = sub_565F70(v19);
    if ( *(_BYTE *)(v20 + 16) )
    {
      v36 = *(qtwebapp::HttpRequest **)(v20 + 8);
      v53 = 0;
      v54 = 0;
      v52 = 2;
      v55 = v36;
      QMessageLogger::debug(&v46, (QMessageLogger *)&v52);
      v37 = sub_4DC040(&v46, "upload request");
      qtwebapp::HttpRequest::getPath(&v49, this);
      sub_568520(v37, &v49);
      sub_4DBF50(&v49);
      QDebug::~QDebug((QDebug *)&v46);
    }
    QObject::QObject((QObject *)&v52, 0);
    v56 = a3;
    v57 = 0;
    v52 = (__int64)&off_FE1818;
    v54 = off_FE1890;
    v55 = this;
    v58 = 0u;
    *(_QWORD *)&v59 = sub_5636A0;
    *((_QWORD *)&v59 + 1) = sub_563690;
    sub_45CF30(&v49, a1 + 16);
    sub_B5F660((__int64)&v58, (__int64 *)&v49);
    if ( (_QWORD)v50 )
      ((void (__fastcall *)(__int128 *, __int128 *, __int64))v50)(&v49, &v49, 3);
    v57 = *(_QWORD *)(a1 + 48);
    sub_56AB50(&v52);
    v52 = (__int64)&off_FE1818;
    v54 = (__int64 (__fastcall **)())&unk_FE17F0;
    if ( !(_QWORD)v59 )
      goto LABEL_13;
    goto LABEL_12;
  }
  qtwebapp::HttpRequest::getPath(&v52, this);
  v21 = v53;
  if ( !v53 )
    v21 = (qtwebapp::HttpRequest *)&QByteArray::_empty;
  v22 = (unsigned __int8)QtPrivate::startsWith(v54, v21, 10, "/download/");
  v23 = sub_4DBF50(&v52);
  if ( v22 )
  {
    v24 = sub_565F70(v23);
    if ( *(_BYTE *)(v24 + 16) )
    {
      v39 = *(qtwebapp::HttpRequest **)(v24 + 8);
      v53 = 0;
      v54 = 0;
      v52 = 2;
      v55 = v39;
      QMessageLogger::debug(&v46, (QMessageLogger *)&v52);
      v40 = sub_4DC040(&v46, "download request");
      qtwebapp::HttpRequest::getPath(&v49, this);
      sub_568520(v40, &v49);
      sub_4DBF50(&v49);
      QDebug::~QDebug((QDebug *)&v46);
    }
    QObject::QObject((QObject *)&v52, 0);
    v56 = a3;
    v57 = 0;
    v52 = (__int64)&off_FE1B28;
    v54 = off_FE1BA0;
    v55 = this;
    v58 = 0u;
    *(_QWORD *)&v59 = sub_5636A0;
    *((_QWORD *)&v59 + 1) = sub_563690;
    sub_45CF30(&v49, a1 + 16);
    sub_B5F660((__int64)&v58, (__int64 *)&v49);
    if ( (_QWORD)v50 )
      ((void (__fastcall *)(__int128 *, __int128 *, __int64))v50)(&v49, &v49, 3);
    v57 = *(_QWORD *)(a1 + 48);
    sub_568260((qtwebapp::HttpRequest **)&v54);
    v52 = (__int64)&off_FE1B28;
    v54 = (__int64 (__fastcall **)())&unk_FE17F0;
    if ( !(_QWORD)v59 )
      goto LABEL_13;
    goto LABEL_12;
  }
  qtwebapp::HttpRequest::getPath(&v52, this);
  v27 = v53;
  if ( !v53 )
    v27 = (qtwebapp::HttpRequest *)&QByteArray::_empty;
  v28 = (unsigned __int8)QtPrivate::startsWith(v54, v27, 8, "/search/");
  v29 = sub_4DBF50(&v52);
  if ( v28 )
  {
    v30 = sub_565F70(v29);
    if ( *(_BYTE *)(v30 + 16) )
    {
      v41 = *(qtwebapp::HttpRequest **)(v30 + 8);
      v53 = 0;
      v54 = 0;
      v52 = 2;
      v55 = v41;
      QMessageLogger::debug(&v46, (QMessageLogger *)&v52);
      v42 = sub_4DC040(&v46, "search request");
      qtwebapp::HttpRequest::getPath(&v49, this);
      sub_568520(v42, &v49);
      sub_4DBF50(&v49);
      QDebug::~QDebug((QDebug *)&v46);
    }
    QObject::QObject((QObject *)&v52, 0);
    v56 = a3;
    v57 = 0;
    v52 = (__int64)&off_FE1960;
    v54 = off_FE19D8;
    v55 = this;
    v58 = 0u;
    *(_QWORD *)&v59 = sub_5636A0;
    *((_QWORD *)&v59 + 1) = sub_563690;
    sub_45CF30(&v49, a1 + 16);
    sub_B5F660((__int64)&v58, (__int64 *)&v49);
    if ( (_QWORD)v50 )
      ((void (__fastcall *)(__int128 *, __int128 *, __int64))v50)(&v49, &v49, 3);
    v57 = *(_QWORD *)(a1 + 48);
    sub_568260((qtwebapp::HttpRequest **)&v54);
    v52 = (__int64)&off_FE1960;
    v54 = (__int64 (__fastcall **)())&unk_FE17F0;
    if ( !(_QWORD)v59 )
      goto LABEL_13;
LABEL_12:
    ((void (__fastcall *)(__int128 *, __int128 *, __int64))v59)(&v58, &v58, 3);
LABEL_13:
    QObject::~QObject((QObject *)&v52);
    return;
  }
  qtwebapp::HttpRequest::getPath(&v52, this);
  v31 = (unsigned __int8)sub_5684A0(&v52, "/");
  sub_4DBF50(&v52);
  if ( v31 )
  {
    v52 = (__int64)&off_FE2158;
    v53 = this;
    v54 = a3;
    v55 = 0;
    v56 = 0;
    v57 = 0;
    *(_QWORD *)&v58 = sub_5636A0;
    *((_QWORD *)&v58 + 1) = sub_563690;
    sub_47BBD0(&v49, "index.html");
    sub_568A70(&v52, &v49);
    sub_47BC70(&v49);
    v52 = (__int64)&unk_FE17F0;
    if ( (_QWORD)v58 )
LABEL_51:
      ((void (__fastcall *)(__int64 (__fastcall ***)(), __int64 (__fastcall ***)(), __int64))v58)(&v56, &v56, 3);
  }
  else
  {
    qtwebapp::HttpRequest::getPath(&v52, this);
    v34 = (unsigned __int8)sub_5684A0(&v52, "/log.txt");
    v35 = sub_4DBF50(&v52);
    if ( v34 )
    {
      v45 = 58;
      v44[0] = xmmword_FE2170;
      v44[1] = xmmword_FE2180;
      v49 = xmmword_FE2170;
      v50 = xmmword_FE2180;
      v51 = 58;
      sub_4DCCA0(sub_565F70, v44);
      v52 = (__int64)&off_FE21A8;
      v53 = this;
      v54 = a3;
      v55 = 0;
      v56 = 0;
      v57 = 0;
      *(_QWORD *)&v58 = sub_5636A0;
      *((_QWORD *)&v58 + 1) = sub_563690;
      sub_45CF30(&v46, a1 + 16);
      sub_B5F660((__int64)&v56, (__int64 *)&v46);
      if ( v47 )
        v47(&v46, &v46, 3);
      v55 = *(qtwebapp::HttpRequest **)(a1 + 48);
      sub_56B7A0(&v52);
      v52 = (__int64)&unk_FE17F0;
      if ( (_QWORD)v58 )
        ((void (__fastcall *)(__int64 (__fastcall ***)(), __int64 (__fastcall ***)(), __int64))v58)(&v56, &v56, 3);
    }
    else
    {
      v38 = sub_565F70(v35);
      if ( *(_BYTE *)(v38 + 16) )
      {
        v43 = *(qtwebapp::HttpRequest **)(v38 + 8);
        v52 = 2;
        v53 = 0;
        v54 = 0;
        v55 = v43;
        QMessageLogger::debug(&v49, (QMessageLogger *)&v52);
        sub_4DC040(&v49, "file request");
        QDebug::~QDebug((QDebug *)&v49);
      }
      v52 = (__int64)&off_FE2158;
      v53 = this;
      v54 = a3;
      v55 = 0;
      v56 = 0;
      v57 = 0;
      *(_QWORD *)&v58 = sub_5636A0;
      *((_QWORD *)&v58 + 1) = sub_563690;
      sub_45CF30(&v49, a1 + 16);
      sub_B5F660((__int64)&v56, (__int64 *)&v49);
      if ( (_QWORD)v50 )
        ((void (__fastcall *)(__int128 *, __int128 *, __int64))v50)(&v49, &v49, 3);
      v55 = *(qtwebapp::HttpRequest **)(a1 + 48);
      sub_569540(&v52);
      v52 = (__int64)&unk_FE17F0;
      if ( (_QWORD)v58 )
        goto LABEL_51;
    }
  }
}

I’ve cleaned up the decompiled pseudocode significantly to improve readability. I’ve also eliminated the low-level memory management details and Qt-specific implementation noise. Here is a high-level overview of the HttpRequestRouter function:

void HttpRequestRouter(ServerContext* context, HttpRequest* request, ResponseHandler** responseHandler)
{
    QString requestPath;
    
    // Get the request path
    request->getPath(&requestPath);
    
    // Route: /documents/
    if (requestPath.startsWith("/documents/")) {
        LogManager* logger = GetLogger();
        if (logger->isDebugEnabled()) {
            logger->debug() << "list request" << request->getPath();
        }
        
        // Create document list handler
        DocumentListHandler* handler = new DocumentListHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        
        handler->processRequest();
        return;
    }
    
    // Route: /thumbnail/
    if (requestPath.startsWith("/thumbnail/")) {
        LogManager* logger = GetLogger();
        if (logger->isDebugEnabled()) {
            logger->debug() << "thumbnail request" << request->getPath();
        }
        
        // Create thumbnail handler
        ThumbnailHandler* handler = new ThumbnailHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        
        handler->processRequest();
        return;
    }
    
    // Route: /upload
    if (requestPath == "/upload") {
        LogManager* logger = GetLogger();
        if (logger->isDebugEnabled()) {
            logger->debug() << "upload request" << request->getPath();
        }
        
        // Create upload handler
        UploadHandler* handler = new UploadHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        
        handler->processRequest();
        return;
    }
    
    // Route: /download/
    if (requestPath.startsWith("/download/")) {
        LogManager* logger = GetLogger();
        if (logger->isDebugEnabled()) {
            logger->debug() << "download request" << request->getPath();
        }
        
        // Create download handler
        DownloadHandler* handler = new DownloadHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        
        handler->processRequest();
        return;
    }
    
    // Route: /search/
    if (requestPath.startsWith("/search/")) {
        LogManager* logger = GetLogger();
        if (logger->isDebugEnabled()) {
            logger->debug() << "search request" << request->getPath();
        }
        
        // Create search handler
        SearchHandler* handler = new SearchHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        
        handler->processRequest();
        return;
    }
    
    // Route: / (root - serve index.html)
    if (requestPath == "/") {
        StaticFileHandler* handler = new StaticFileHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->serveFile("index.html");
        return;
    }
    
    // Route: /log.txt (special logging endpoint)
    if (requestPath == "/log.txt") {
        // Set content type headers for text/plain
        HttpHeaders headers;
        headers.setContentType("text/plain; charset=utf-8");
        
        LogFileHandler* handler = new LogFileHandler();
        handler->setRequest(request);
        handler->setResponseHandler(responseHandler);
        handler->setContext(context);
        handler->setHeaders(headers);
        
        handler->processRequest();
        return;
    }
    
    // Default route: serve static files
    LogManager* logger = GetLogger();
    if (logger->isDebugEnabled()) {
        logger->debug() << "file request";
    }
    
    StaticFileHandler* handler = new StaticFileHandler();
    handler->setRequest(request);
    handler->setResponseHandler(responseHandler);
    handler->setContext(context);
    
    handler->processRequest();
}

This function is essentially acts as an HTTP request router/dispatcher built on top of qtwebapp::HttpRequest. It checks the request path (getPath()) and compares it to different known prefixes:

  • /documents/ document listing functionality
  • /thumbnail/ thumbnail generation and serving
  • /upload/ file upload handling
  • /download/ file download functionality
  • /search/ search functionality
  • / serves the main index.html page
  • /log.txt special endpoint for accessing log files

By default, it serves static files for any other requests.

The code follows a handler pattern, with each route type having its own dedicated handler class to process specific requests. While this is a common and clean architecture for web servers, it makes it harder for us to reverse the code logic.

HTTP request handler analysis

After some research, I discovered how the handler setup works in detail. Here is what I learned: When a thumbnail request is detected, the code:

  • creates a handler object with the vtable off_FE18B8 and function pointers off_FE1930
  • sets up the request context and stores the HTTP request object and callback functions.
  • calls initialisation via sub_45CF30 and sub_B5F660
  • invokes the handler via sub_568260
void __fastcall sub_568260(void (__fastcall ***a1)(qtwebapp::HttpRequest **, void **))
{
  // variable declarations ...

  qtwebapp::HttpRequest *req = (qtwebapp::HttpRequest *)callbackTable[1];
  qtwebapp::HttpRequest::getPath(v9, req);

  QString::fromUtf8(v8, v9[2], v9[1]);
  
  // Split the path on '/'
  QString::fromUtf8(v10, 1, "/");
  QString::split(&ptr, v8, v10, 1);
  
  // Free the temporary buffers (if we are in debug mode)
  if ( v10[0] && (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
    free(v10[0]);
  if ( v8[0] && (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
    free(v8[0]);
  if ( v9[0] && (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
    free(v9[0]);

  // Defensive clean‑up of the split list
  if ( !ptr || *(int *)ptr > 1 )
    sub_B6FF30((__int64)&ptr, 0, 0, 0);

  // Call the user callback
  v2 = *v6;
  v6 += 3;
  --v7;
  if ( v2 && (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
    free(v2);
  (*callbackTable)[3]((qtwebapp::HttpRequest **)callbackTable, &ptr);

  // Post‑callback clean‑up
  if ( ptr && (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
  {
    v3 = v6;
    v4 = &v6[3 * v7];
    if ( v6 != v4 )
    {
      do
      {
        if ( *v3 )
        {
          if ( (unsigned int)sub_DDC440(0xFFFFFFFFLL) == 1 )
            free(*v3);
        }
        v3 += 3;
      }
      while ( v4 != v3 );
    }
    free(ptr);
  }
}

Looking at sub_568260 and the vtable off_FE1930, the following is happening: It gets the HTTP request path using qtwebapp::HttpRequest::getPath(), converts it to a QString, splits the path using the / delimiter to extract the path components, skips the first component (which is probably “thumbnail”) and extracts what appears to be a file identifier from the remaining path. After parsing the path, it calls:

(*callbackTable)[3]((qtwebapp::HttpRequest **)callbackTable, &ptr)

This invokes the 4th function in the vtable off_FE1930:

.rodata:0xFE1930 off_FE1930  DCQ sub_563950  // Constructor/destructor function
.rodata:0xFE1938             DCQ sub_563DA0  // Likely another lifecycle function
.rodata:0xFE1940             DCQ sub_568260  // The path parsing function we just analysed
.rodata:0xFE1948             DCQ sub_56AB40  // The actual thumbnail generation function
... more rodata

which is sub_56AB40:

__int64 __fastcall sub_56AB40(__int64 a1)
{
  return sub_56A7A0(a1 - 16);
}

__int64 __fastcall sub_56A7A0(__int64 a1, __int64 a2)
{
  // variable declarations ...

  // number of thumbnails
  v3 = *(_QWORD *)(a2 + 16);
  if ( v3 )
  {
    // pointer to the array of thumbnail IDs
    v4 = *(_QWORD *)(a2 + 8);
    // v20 is a two‑element array that will receive the callback data
    v20[0] = 0;
    v20[1] = 0;
    v5 = v4 + 24 * v3 - 24;
    // flag that will be set by the callback to indicate success
    v21 = 0;

    // create a QEventLoop on the stack (v18 points to it)
    QEventLoop::QEventLoop((QEventLoop *)v18, 0);
    v14 = sub_DDC630(1, &unk_15D2678);
    v19[0] = sub_564850;
    v19[1] = 0;
    v6 = *(_QWORD *)(a1 + 40);
    // allocate 0x28 bytes (40 bytes) for the callback object
    unsigned __int64 v7 = operator new(0x28u);
    *(_DWORD *)v7 = 1;                    // type byte
    *(_QWORD *)(v7 + 8) = sub_565D30;     // callback function
    *(_QWORD *)(v7 + 16) = &v14;          // user data
    *(_QWORD *)(v7 + 24) = v20;           // shared result buffer
    *(_QWORD *)(v7 + 32) = v18;           // event loop

    // Connect the callback to the QObject that owns the thumbnail logic
    QObject::connectImpl(&v15, v6, v19, a1, 0, v7, 2, &unk_FE2050, &off_FE1628);
    QMetaObject::Connection::~Connection((QMetaObject::Connection *)&v15);

    // Invoke the method onRequestThumbnailPath() on the QObject
    v8 = *(_QWORD *)(a1 + 40);
    v22[0] = 0;
    v22[1] = &v14;
    v22[2] = v5;
    v23[0] = 0;
    v23[1] = "quint64";
    v23[2] = "QString";
    v24[0] = 0;
    v24[1] = &QtPrivate::QMetaTypeInterfaceWrapper<unsigned long long>::metaType;
    v24[2] = &QtPrivate::QMetaTypeInterfaceWrapper<QString>::metaType;
    QMetaObject::invokeMethodImpl(v8, "onRequestThumbnailPath", 2, 3, v22, v23, v24);

    // Run the event loop until the callback quits it
    QEventLoop::exec(v18, 0);

    // Handle the result that was delivered by the callback
    if ( v21 )
    {
      v9 = qword_15D1FD8;
      // The first sub‑call verifies that the result data is valid
      if ( (*(unsigned __int8 (__fastcall **)(__int64, _QWORD *))(*(_QWORD *)qword_15D1FD8 + 112LL))(qword_15D1FD8, v20) )
      {
        (*(void (__fastcall **)(__int64 *__return_ptr, __int64, _QWORD *))(*(_QWORD *)v9 + 272LL))(&v16, v9, v20);
        v10 = a1 + 16;
        // The second sub‑call asks whether the data is valid
        if ( (*(unsigned __int8 (__fastcall **)(__int64, __int64))(*(_QWORD *)v16 + 48LL))(v16, 1) )
        {
          // The thumbnail is valid – build a QByteArray containing the image data
          v11 = v16;
          QByteArray::QByteArray((QByteArray *)v22, "image/jpeg", -1);
          sub_AFB9E0(v10, v11, v22);
          sub_4DBF50(v22);
          if ( !v17 )
          {
LABEL_7:
            // Destroy the event loop (even though it is normally already destroyed after exec() returned)
            QEventLoop::~QEventLoop((QEventLoop *)v18);
            return sub_47BC70(v20);
          }
        }
        else
        {
          // The data could not be retrieved
          sub_47BBD0(v22, "Unable to open thumbnail");
          sub_568590(v10, v22, 500);
          sub_47BC70(v22);
          if ( !v17 )
            goto LABEL_7;
        }
        sub_47F280();
        goto LABEL_7;
      }
      // The request itself failed
      sub_47BBD0(v22, "Unable to get thumbnail");
      sub_568590(a1 + 16, v22, 500);
    }
    else
    {
      sub_47BBD0(v22, "Unable to get thumbnail for entry");
      sub_568590(a1 + 16, v22, 500);
    }
    sub_47BC70(v22);
    goto LABEL_7;
  }
  v13 = a1 + 16;
  do
    ++v3;
  while ( aMissingThumbna[v3] );
  // Build an error string that contains the missing ID
  QString::fromUtf8(v22, v3, "Missing thumbnail ID");
  sub_568590(v13, v22, 400);
  return sub_47BC70(v22);
}

This function finally gives us a complete flow for handling thumbnail requests. The step-by-step process is as follows:

1. Request validation

if ( v3 )  // Check if we have path components from URL parsing

If no thumbnail ID is provided in the URL, returns Missing thumbnail ID with an HTTP 400 error.

2. Asynchronous processing setup

It creates a QEventLoop to wait synchronously for an asynchronous thumbnail‑request callback and sets up Qt signal-slot connections for handling the response. The connection links completion signals to the event loop.

3. Thumbnail request invocation

cppQMetaObject::invokeMethodImpl(v8, "onRequestThumbnailPath", 2, 3, v22, v23, v24);

The onRequestThumbnailPath method is called via QMetaObject::invokeMethodImpl(), passing the thumbnail ID/path extracted from the URL and uses Qt’s meta-object system for dynamic method invocation.

4. Event loop wait

QEventLoop::exec(v18, 0);

This blocks execution until the thumbnail generation is complete. During this time, the event loop processes Qt signals and slots.

5. Response handling

After the event loop has finished processing, the code checks the result.

if ( v21 )  // If thumbnail generation succeeded
{
    // Check if thumbnail exists in storage
    if ( (*(unsigned __int8 (__fastcall **)(__int64, _QWORD *))(*(_QWORD *)qword_15D1FD8 + 112LL))(qword_15D1FD8, v20) )
    {
        // Get thumbnail data from storage
        (*(void (__fastcall **)(__int64 *__return_ptr, __int64, _QWORD *))(*(_QWORD *)v9 + 272LL))(&v16, v9, v20);
        
        // Verify thumbnail can be opened
        if ( (*(unsigned __int8 (__fastcall **)(__int64, __int64))(*(_QWORD *)v16 + 48LL))(v16, 1) )
        {
            // Send thumbnail with JPEG content type
            QByteArray::QByteArray((QByteArray *)v22, "image/jpeg", -1);
            sub_AFB9E0(v10, v11, v22);  // Send HTTP response with image data
        }
    }
}

If successful, it returns the JPEG byte array; if unsuccessful, it returns a human-readable error message:

  • Unable to open thumbnail (HTTP 500): the thumbnail exists but cannot be read
  • Unable to open thumbnail (HTTP 500): the thumbnail exists but cannot be read
  • Unable to get thumbnail (HTTP 500): the thumbnail generation/retrieval failed
  • Unable to get thumbnail for entry (HTTP 500): no valid entry was found for the ID

However, this function doesn’t perform the rendering, it just integrates the storage! qword_15D1FD8 appears to be a storage manager/database interface.

  • Method 112: check if the thumbnail exists.
  • Method 272: retrieve thumbnail data.
  • Method 48: verify that the thumbnail can be opened.

Actual image processing, such as resizing and format conversion, happens in the onRequestThumbnailPath method, which is called asynchronously. For some reason, the method cannot be found in the usual binary. I tried searching for it using the find command:

root@imx8mm-ferrari:~# find /usr/bin -type f -exec sh -c 'strings "$1" | grep -q "onRequestThumbnailPath" && echo "$1"' _ {} \;
/usr/bin/xochitl

The string only appears in Xochitl; I couldn’t find any other mentions of onRequestThumbnailPath on the device.

Searching for render in the file path provides a clue:

root@imx8mm-ferrari:~# find / -iname "*render*"
/usr/bin/xochitl_pdf_renderer

Searching for the thumbnail in the file path provides a clearer indication:

root@imx8mm-ferrari:~# find / -iname "*thumbnail*"
/home/root/.local/share/remarkable/xochitl/ddfd157a-a450-4bde-8773-01d1be5f4006.thumbnails
... list goes one

Here is the directory structure for an .epub file that has been imported on the device:

root@imx8mm-ferrari:~# tree /home/root/.local/share/remarkable/xochitl/ | grep ddfd157a-a450-4bde-8773-01d1be5f4006   
├── ddfd157a-a450-4bde-8773-01d1be5f4006
├── ddfd157a-a450-4bde-8773-01d1be5f4006.content
├── ddfd157a-a450-4bde-8773-01d1be5f4006.epub
├── ddfd157a-a450-4bde-8773-01d1be5f4006.epubindex
├── ddfd157a-a450-4bde-8773-01d1be5f4006.local
├── ddfd157a-a450-4bde-8773-01d1be5f4006.metadata
├── ddfd157a-a450-4bde-8773-01d1be5f4006.pagedata
├── ddfd157a-a450-4bde-8773-01d1be5f4006.pdf
└── ddfd157a-a450-4bde-8773-01d1be5f4006.thumbnails
    └── 8eae075c-8cf7-4ecd-adcf-86050a3062ab.png

Here is the one for a document created on the device:

├── 63b02eef-b19f-4238-83b9-861a0492e22f
│   ├── 69c51756-5c00-4b16-8608-8c5a2c488f9d.rm
│   ├── 7ff49c9f-5563-4be5-a813-ae059c7ab9a2.rm
│   ├── afe73385-2748-4294-baea-eb64a9a33022.rm
│   └── d039e834-06a2-4f26-ab93-8e4a60fa759a.rm
├── 63b02eef-b19f-4238-83b9-861a0492e22f.content
├── 63b02eef-b19f-4238-83b9-861a0492e22f.local
├── 63b02eef-b19f-4238-83b9-861a0492e22f.metadata
└── 63b02eef-b19f-4238-83b9-861a0492e22f.thumbnails
    ├── 69c51756-5c00-4b16-8608-8c5a2c488f9d.png
    ├── 7ff49c9f-5563-4be5-a813-ae059c7ab9a2.png
    ├── afe73385-2748-4294-baea-eb64a9a33022.png
    └── d039e834-06a2-4f26-ab93-8e4a60fa759a.png

.local:

{
    "contentFormatVersion": 2
}

.metadata:

{
    "createdTime": "1747459907027",
    "lastModified": "1755508309462",
    "lastOpened": "1755508275358",
    "lastOpenedPage": 3,
    "new": false,
    "parent": "",
    "pinned": false,
    "source": "",
    "type": "DocumentType",
    "visibleName": "Planning"
}

.content:

{
    "cPages": {
        "lastOpened": {
            "timestamp": "1:12",
            "value": "afe73385-2748-4294-baea-eb64a9a33022"
        },
        "original": {
            "timestamp": "0:0",
            "value": -1
        },
        "pages": [
            {
                "id": "d039e834-06a2-4f26-ab93-8e4a60fa759a",
                "idx": {
                    "timestamp": "1:2",
                    "value": "ba"
                },
                "scrollTime": {
                    "timestamp": "1:2",
                    "value": "2025-05-19T21:59:35Z"
                },
                "template": {
                    "timestamp": "1:5",
                    "value": "P Week 2"
                },
                "verticalScroll": {
                    "timestamp": "1:2",
                    "value": 1620
                }
            },
            {
                "id": "7ff49c9f-5563-4be5-a813-ae059c7ab9a2",
                "idx": {
                    "timestamp": "1:2",
                    "value": "bb"
                },
                "template": {
                    "timestamp": "1:3",
                    "value": "P Checklist"
                }
            }
            // other pages
        ],
        "uuids": [
            {
                "first": "3d3d4296-41b3-5a7f-87b7-8d4cba70bae4",
                "second": 1
            }
        ]
    },
    "coverPageNumber": -1,
    "customZoomCenterX": 0,
    "customZoomCenterY": 936,
    "customZoomOrientation": "portrait",
    "customZoomPageHeight": 1872,
    "customZoomPageWidth": 1404,
    "customZoomScale": 1,
    "documentMetadata": {
    },
    "extraMetadata": {
        "LastActiveTool": "secondary",
        // + metadata added by https://github.com/FouzR/xovi-extensions
    },
    "fileType": "notebook",
    "fontName": "",
    "formatVersion": 2,
    "lineHeight": -1,
    "margins": 125,
    "orientation": "portrait",
    "pageCount": 4,
    "pageTags": [
    ],
    "sizeInBytes": "684504",
    "tags": [
    ],
    "textAlignment": "justify",
    "textScale": 1,
    "zoomMode": "bestFit"
}

The .thumbnails folder contains 384x512 thumbnails for each page.

The main folder contains .rm files: reMarkable.lines, version=6 and the raw content. rm-file.png This contains vector-based drawing data. Here’s how the core rendering works:

Understanding the document format

Header analysis:

72654D61 726B6162 6C65 = "reMarkable"
202E6C69 6E657320 6669 6C65 = " .lines file"
2C207665 7273696F 6E3D36 = ", version=6"

This header is used to identify the file version and was found in the .rodata section of Xochitl, in a function related to writeSceneFileV6.

Here is the core rendering process that I inferred: The file contains coordinate data for pen strokes. Each stroke is represented as a series of points with pressure and width information. Then, the rendering system reconstructs smooth curves from these discrete points.

By analysing the hex data, we can infer the structure and identify a repeating pattern:

$ xxd -g 1 -c 16 file.rm  | less
00000000: 72 65 4d 61 72 6b 61 62 6c 65 20 2e 6c 69 6e 65  reMarkable .line
00000010: 73 20 66 69 6c 65 2c 20 76 65 72 73 69 6f 6e 3d  s file, version=
00000020: 36 20 20 20 20 20 20 20 20 20 20 19 00 00 00 00  6          .....
00000030: 01 01 09 01 0c 13 00 00 00 10 3d 3d 42 96 41 b3  ..........==B.A.
00000040: 5a 7f 87 b7 8d 4c ba 70 ba e4 01 00 07 00 00 00  Z....L.p........
00000050: 00 01 01 00 1f 01 01 21 01 31 00 19 00 00 00 00  .......!.1......
00000060: 00 01 0a 14 07 00 00 00 24 00 00 00 00 34 00 00  ........$....4..
00000070: 00 00 44 00 00 00 00 54 00 00 00 00 6e 00 00 00  ..D....T....n...
00000080: 00 00 01 0d 1c 06 00 00 00 1f 00 00 2f 00 00 2c  ............/..,
00000090: 05 00 00 00 1f 00 00 21 01 3c 05 00 00 00 1f 00  .......!.<......
000000a0: 00 21 01 5c 08 00 00 00 54 06 00 00 70 08 00 00  .!.\....T...p...

Once the header is removed, the hex data shows repeating patterns:

xxd -g 1 -c 14 file.bin  | less
00000000: 0c 00 00 0a e1 5f 17 c4 a9 c3 93 43 09 00  ....._.....C..
0000000e: 0c 00 b4 1a d3 a9 17 c4 3f ae 92 43 07 00  ........?..C..
0000001c: 0c 00 ab 20 d2 bc 17 c4 b1 3d 92 43 02 00  ... .....=.C..
0000002a: 0c 00 ab 21 51 b7 17 c4 a5 3e 92 43 01 00  ...!Q....>.C..
00000038: 0c 00 04 22 e6 b2 17 c4 56 57 92 43 02 00  ..."....VW.C..
00000046: 0c 00 32 3e 8e b0 17 c4 5e 6d 92 43 02 00  ..2>....^m.C..
00000054: 0c 00 37 55 d6 9f 17 c4 74 cb 92 43 06 00  ..7U....t..C..
00000062: 0c 00 33 72 a4 70 17 c4 9a 6a 94 43 08 00  ..3r.p...j.C..
00000070: 0c 00 38 87 49 ce 16 c4 44 2d 9e 43 0a 00  ..8.I...D-.C..
0000007e: 0c 00 3a 9c f3 7a 16 c4 ca 4b a1 43 05 00  ..:..z...K.C..
0000008c: 0c 00 34 9d d8 26 16 c4 42 85 a2 43 02 00  ..4..&..B..C..
0000009a: 0c 00 1d 9c c9 da 16 c4 a1 fb 9f 43 06 00  ...........C..
000000a8: 0c 00 ac a2 3c a8 17 c4 5d 59 9c 43 0d 00  ....<...]Y.C..
000000b6: 0c 00 b1 a9 1c 8d 18 c4 ed 9b 98 43 0f 00  ...........C..
000000c4: 0c 00 ae ae 7a 6f 19 c4 ca 96 92 43 06 00  ....zo.....C..
000000d2: 0d 00 b7 b6 2d 66 19 c4 54 77 90 43 04 00  ....-f..Tw.C..
000000e0: 0e 00 be c2 96 1d 19 c4 0e 52 8f 43 03 00  .........R.C..
000000ee: 0e 00 d3 c6 7d f5 18 c4 4e 12 8f 43 04 00  ....}...N..C..
000000fc: 0e 00 e4 c6 64 9b 17 c4 9d 82 8e 43 08 00  ....d......C..
0000010a: 0e 00 f7 c3 cc 0c 17 c4 fa 8e 8e 43 07 00  ...........C..
00000118: 0e 00 02 c2 a7 28 16 c4 4a 06 8f 43 07 00  .....(..J..C..
00000126: 0e 00 09 c2 2c 4b 15 c4 e1 c9 8f 43 06 00  ....,K.....C..

Each 14-byte block appears to contain the following:

  • tool/stroke type (1 byte);
  • X coordinate (4-byte float)
  • Y coordinate (4-byte float)
  • Pressure/width (4-byte float)
  • additional metadata (1 byte).

I didn’t want to delve into that file any further, but you can find more information in the rmscene and rmc projects, which reimplement the Python conversion.

The thumbnail generation algorithm

Let’s go back to my original investigation. Searching for thumbnail in the strings returns a lot of results, but one of them looks interesting: doGenerateThumbnail: page=%d, %s, %s mentioned in sub_8FEA30.

See the raw pseudo code
void __fastcall sub_8FEA30(__int64 a1, __int64 *a2, unsigned int a3, __int64 a4)
{
  // variable declarations ...

  v4 = (pthread_mutex_t *)(a1 + 200);
  v9 = pthread_mutex_lock((pthread_mutex_t *)(a1 + 200));
  if ( v9 )
    std::__throw_system_error(v9);
  v10 = 1;
  v11 = *(_QWORD *)(sub_A1BCE0(a1 + 248, a3) + 120);
  pthread_mutex_unlock(v4);
  v12 = *a2;
  if ( !v11 )
  {
    v10 = 0;
    if ( *(double *)(v12 + 712) > 0.0 )
      v10 = *(double *)(v12 + 720) > 0.0;
  }
  if ( !*(_BYTE *)(a1 + 836) || (v21 = sub_8F10E0(a1, a3), (v21 & 0x80000000) != 0) )
  {
    v13 = atomic_load((unsigned int *)(a1 + 840));
    v14 = v13 == 1;
  }
  else
  {
    v22 = (*(double (__fastcall **)(_QWORD, _QWORD))(**(_QWORD **)(a1 + 1064) + 192LL))(*(_QWORD *)(a1 + 1064), v21);
    v14 = v22 > v23;
  }
  v15 = sub_8EAE00();
  if ( *(_BYTE *)(v15 + 16) )
  {
    v55 = "background";
    v56 = *(_QWORD *)(v15 + 8);
    if ( v10 )
      v55 = "scene";
    v84 = 2u;
    *(_QWORD *)&v85 = 0;
    v57 = "portrait";
    if ( v14 )
      v57 = "landscape";
    *((_QWORD *)&v85 + 1) = v56;
    QMessageLogger::debug((QMessageLogger *)&v84, " -> doGenerateThumbnail: page=%d, %s, %s", a3, v55, v57);
  }
  if ( *(_QWORD *)(a1 + 1112) && !(*(unsigned __int8 (**)(void))(a1 + 1120))() )
  {
    v51 = sub_8EAE00();
    if ( *(_BYTE *)(v51 + 17) )
    {
      v45 = a3;
      v46 = "Not enough space to store thumbnails, page=%d";
      v47 = *(_QWORD *)(v51 + 8);
      goto LABEL_52;
    }
  }
  else
  {
    v16 = atomic_load((unsigned __int8 *)(a1 + 1025));
    if ( !v16 || *(_BYTE *)(a1 + 1092) )
    {
      v65.n64_u64[0] = *(unsigned __int64 *)(a1 + 1072);
      if ( v14 )
      {
        v65.n64_u64[0] = vrev64_s32(v65).n64_u64[0];
        QImage::QImage((QImage *)v71);
        if ( v10 )
          goto LABEL_14;
      }
      else
      {
        QImage::QImage((QImage *)v71);
        if ( v10 )
        {
LABEL_14:
          if ( *(_BYTE *)(v12 + 352) )
          {
            v17 = *(double *)(v12 + 336);
            v18 = *(double *)(v12 + 344);
          }
          else
          {
            v17 = *(double *)(v12 + 320);
            v18 = *(double *)(v12 + 328);
          }
          if ( v17 <= 0.0 || v18 <= 0.0 )
          {
            v20 = 1872;
            v19 = 1404;
          }
          else
          {
            v19 = llround(v17);
            v20 = llround(v18);
          }
          if ( v14 )
            v29 = v20;
          else
            v29 = v19;
          if ( !v14 )
            v19 = v20;
          v88 = 0x3FF0000000000000LL;
          v84 = xmmword_1303AB0;
          v85 = xmmword_1303AC0;
          v86 = xmmword_1303AD0;
          v87 = xmmword_1303AE0;
          v90 = 0;
          v89 &= 0xFC00u;
          v91 = 7;
          QColor::QColor(v92, 3);
          v94 = 0;
          v96 = 1;
          v30 = *(unsigned __int8 *)(a1 + 1092) ^ 1;
          v90 = __PAIR64__(v19, v29);
          LODWORD(v91) = 7;
          HIDWORD(v91) = v30;
          v95 = 65792;
          if ( !v14 && *(_BYTE *)(v12 + 813) )
          {
            v58 = sub_96BD90(v12);
            v34 = v59;
            v37 = v60;
            v80 = v58;
            v81 = v61;
            v82 = v60;
            v83 = v59;
          }
          else
          {
            v31 = sub_96BE10(v12);
            v32 = *(unsigned __int8 *)(v12 + 792);
            v34 = v33;
            v80 = v31;
            v81 = v35;
            v37 = v36;
            v82 = v36;
            v83 = v33;
            if ( v32 )
            {
              v37 = *(double *)(v12 + 776);
              v34 = *(double *)(v12 + 784);
            }
          }
          v38 = (double)v29 / v37;
          v39 = ((double)v19 - v38 * v34) * 0.5;
          if ( v39 <= 0.0 )
            v39 = 0.0;
          QTransform::translate((QTransform *)&v84, (double)v29 * 0.5, v39);
          QTransform::scale((QTransform *)&v84, v38, v38);
          sub_8FE030(v73, a1, a2, a3, &v84, 0, 0);
          QPaintDevice::QPaintDevice((QPaintDevice *)&v77);
          v77 = &unk_15AAB10;
          v40 = v72;
          v72 = v74;
          v74 = 0;
          v79 = v40;
          QImage::~QImage((QImage *)&v77);
          QImage::~QImage((QImage *)v73);
          QImage::scaled(v75, v71, &v65, 2, 1);
          QPaintDevice::QPaintDevice((QPaintDevice *)&v77);
          v41 = v72;
          v72 = v76;
          v76 = 0;
          v77 = &unk_15AAB10;
          v79 = v41;
          QImage::~QImage((QImage *)&v77);
          QImage::~QImage((QImage *)v75);
          if ( v94 )
          {
            v94 = 0;
            sub_47BC70(v93);
          }
          goto LABEL_44;
        }
      }
      v24 = sub_8F10E0(a1, a3);
      v25 = v24;
      if ( (v24 & 0x80000000) == 0 )
      {
        LODWORD(v84) = llround(
                         (*(double (__fastcall **)(_QWORD, _QWORD))(**(_QWORD **)(a1 + 1064) + 192LL))(
                           *(_QWORD *)(a1 + 1064),
                           v24));
        DWORD1(v84) = llround(v26);
        v27 = QSize::scaled(&v84, &v65, 1);
        (*(void (__fastcall **)(__int128 *__return_ptr, _QWORD, _QWORD, __int64, double, double, double, double))(**(_QWORD **)(a1 + 1064) + 120LL))(
          &v84,
          *(_QWORD *)(a1 + 1064),
          v25,
          v27,
          0.0,
          0.0,
          (double)(int)v27,
          (double)SHIDWORD(v27));
        v28 = BYTE8(v85);
        if ( !BYTE8(v85) )
        {
          QImage::operator=(v71, &v84);
          v28 = BYTE8(v85);
        }
        if ( v28 != 255 )
          ((void (__fastcall *)(double *, __int128 *))`vtable for'std::_Sp_counted_ptr_inplace<Page,std::allocator<Page>,(__gnu_cxx::_Lock_policy)2>[(char)v28 + 59])(
            &v80,
            &v84);
      }
      if ( (unsigned __int8)QImage::isNull((QImage *)v71) )
      {
        QImage::QImage(&v84, &v65, 7);
        sub_4FE220(v71, &v84);
        QImage::~QImage((QImage *)&v84);
        QImage::fill(v71, 3);
      }
LABEL_44:
      v42 = qword_15D1FD8;
      sub_A12AA0(v70, a1 + 152, a4);
      *(_QWORD *)&v84 = 0;
      *((_QWORD *)&v84 + 1) = L"%1.tmp.png";
      *(_QWORD *)&v85 = 10;
      QString::arg(&v67, &v84, v70, 0, 32);
      sub_47BC70(&v84);
      (*(void (__fastcall **)(__int64, __int64 *))(*(_QWORD *)v42 + 208LL))(v42, &v67);
      (*(void (__fastcall **)(_QWORD *__return_ptr, __int64, _QWORD *))(*(_QWORD *)v42 + 32LL))(v66, v42, v70);
      if ( (*(unsigned __int8 (__fastcall **)(__int64, _QWORD *))(*(_QWORD *)v42 + 216LL))(v42, v66) )
      {
        if ( (*(unsigned __int8 (__fastcall **)(__int64, __int64 *, _BYTE *))(*(_QWORD *)v42 + 264LL))(v42, &v67, v71) )
        {
          if ( (*(unsigned __int8 (__fastcall **)(__int64, __int64 *, _QWORD *))(*(_QWORD *)v42 + 192LL))(
                 v42,
                 &v67,
                 v70) )
          {
            LODWORD(v77) = a3;
            v80 = 0.0;
            v81 = &v77;
            QMetaObject::activate((QMetaObject *)a1, (QObject *)&off_FE7038, (const QMetaObject *)1, (int)&v80, &v77);
          }
          else
          {
            v43 = sub_8EAE00();
            if ( *(_BYTE *)(v43 + 17) )
            {
              v62 = *(_QWORD *)(v43 + 8);
              *(_QWORD *)&v85 = 0;
              v84 = 2u;
              *((_QWORD *)&v85 + 1) = v62;
              QString::toLocal8Bit_helper(&v77, v68, v69, v62);
              v63 = v78;
              if ( !v78 )
                v63 = (const char *)&QByteArray::_empty;
              QString::toLocal8Bit_helper(&v80, (QString *)v70[1], (const QChar *)v70[2], (__int64)&QByteArray::_empty);
              v64 = (const char *)v81;
              if ( !v81 )
                v64 = (const char *)&QByteArray::_empty;
              QMessageLogger::warning(
                (QMessageLogger *)&v84,
                "Failed to move thumbnail into place (%s -> %s), page=%d",
                v63,
                v64,
                a3);
              sub_4DBF50(&v80);
              sub_4DBF50(&v77);
            }
          }
          goto LABEL_48;
        }
        v48 = sub_8EAE00();
        if ( *(_BYTE *)(v48 + 17) )
        {
          v49 = *(_QWORD *)(v48 + 8);
          *(_QWORD *)&v85 = 0;
          v84 = 2u;
          *((_QWORD *)&v85 + 1) = v49;
          QString::toLocal8Bit_helper(&v80, v68, v69, v49);
          v50 = (const char *)v81;
          if ( !v81 )
            v50 = (const char *)&QByteArray::_empty;
          QMessageLogger::warning((QMessageLogger *)&v84, "Failed to write temporary thumbnail %s for page=%d", v50, a3);
LABEL_63:
          sub_4DBF50(&v80);
        }
      }
      else
      {
        v52 = sub_8EAE00();
        if ( *(_BYTE *)(v52 + 17) )
        {
          v53 = *(_QWORD *)(v52 + 8);
          *(_QWORD *)&v85 = 0;
          v84 = 2u;
          *((_QWORD *)&v85 + 1) = v53;
          QString::toLocal8Bit_helper(&v80, (QString *)v66[1], (const QChar *)v66[2], v53);
          v54 = (const char *)v81;
          if ( !v81 )
            v54 = (const char *)&QByteArray::_empty;
          QMessageLogger::warning(
            (QMessageLogger *)&v84,
            "Failed to create thumbnail directory %s for page=%d",
            v54,
            a3);
          goto LABEL_63;
        }
      }
LABEL_48:
      sub_47BC70(v66);
      sub_47BC70(&v67);
      sub_47BC70(v70);
      QImage::~QImage((QImage *)v71);
      return;
    }
    v44 = sub_8EAE00();
    if ( *(_BYTE *)(v44 + 17) )
    {
      v45 = a3;
      v46 = "Clients should not request thumbs for a locked document, page=%d";
      v47 = *(_QWORD *)(v44 + 8);
LABEL_52:
      v84 = 2u;
      *(_QWORD *)&v85 = 0;
      *((_QWORD *)&v85 + 1) = v47;
      QMessageLogger::warning((QMessageLogger *)&v84, v46, v45);
    }
  }
}

Here’s a more high-level pseudo code:

void ThumbnailGenerator::generateThumbnail(DocumentContext* context, 
                                         Document** documentPtr, 
                                         unsigned int pageNumber, 
                                         const QString& outputPath)
{
    // Lock the document context for thread safety
    pthread_mutex_t* contextMutex = reinterpret_cast<pthread_mutex_t*>(context + 200);
    int lockResult = pthread_mutex_lock(contextMutex);
    if (lockResult != 0) {
        std::__throw_system_error(lockResult);
    }
    
    // Determine if this is a scene (foreground) page
    bool isScenePage = true;
    uint64_t pageInfo = getPageInfo(context + 248, pageNumber);
    pthread_mutex_unlock(contextMutex);
    
    Document* document = *documentPtr;
    if (!pageInfo) {
        isScenePage = false;
        // Check if document has valid dimensions for background rendering
        if (document->width > 0.0) {
            isScenePage = document->height > 0.0;
        }
    }
    
    // Determine orientation (landscape vs portrait)
    bool isLandscapeOrientation = false;
    if (!context->isDocumentLocked || !hasValidPageData(context, pageNumber)) {
        // Use atomic load to check orientation state
        unsigned int orientationState = atomic_load(&context->orientationFlag);
        isLandscapeOrientation = (orientationState == 1);
    } else {
        // Calculate aspect ratio from document renderer
        DocumentRenderer* renderer = context->renderer;
        double aspectRatio = renderer->getPageAspectRatio(pageNumber);
        isLandscapeOrientation = (aspectRatio > 1.0);
    }
    
    // Debug logging
    Logger* logger = getGlobalLogger();
    if (logger->isDebugEnabled()) {
        const char* pageType = isScenePage ? "scene" : "background";
        const char* orientation = isLandscapeOrientation ? "landscape" : "portrait";
        logger->debug("-> generateThumbnail: page=%d, %s, %s", 
                     pageNumber, pageType, orientation);
    }
    
    // Check storage space availability
    if (context->storageManager && !context->storageManager->hasEnoughSpace()) {
        if (logger->isWarningEnabled()) {
            logger->warning("Not enough space to store thumbnails, page=%d", pageNumber);
        }
        return;
    }
    
    // Check if document is locked
    if (!atomic_load(&context->isUnlocked) || context->requiresAuthentication) {
        if (logger->isWarningEnabled()) {
            logger->warning("Clients should not request thumbs for a locked document, page=%d", 
                          pageNumber);
        }
        return;
    }
    
    // Get target thumbnail dimensions
    QSize targetSize = context->thumbnailSize;
    if (isLandscapeOrientation) {
        targetSize = QSize(targetSize.height(), targetSize.width()); // Swap dimensions
    }
    
    QImage thumbnailImage;
    
    if (isScenePage) {
        // Render scene/foreground content
        generateSceneThumbnail(document, targetSize, isLandscapeOrientation, thumbnailImage);
    } else {
        // Render background/document content
        generateBackgroundThumbnail(context, document, pageNumber, targetSize, thumbnailImage);
    }
    
    // Ensure we have a valid image
    if (thumbnailImage.isNull()) {
        // Create a default placeholder image
        thumbnailImage = QImage(targetSize, QImage::Format_RGB32);
        thumbnailImage.fill(QColor(255, 255, 255)); // White background
    }
    
    // Save thumbnail to file
    saveThumbnailToFile(context, thumbnailImage, pageNumber, outputPath);
}

void ThumbnailGenerator::generateSceneThumbnail(Document* document, 
                                              const QSize& targetSize,
                                              bool isLandscape,
                                              QImage& outputImage)
{
    // Get document dimensions
    double docWidth, docHeight;
    if (document->hasCustomDimensions) {
        docWidth = document->customWidth;
        docHeight = document->customHeight;
    } else {
        docWidth = document->standardWidth;
        docHeight = document->standardHeight;
    }
    
    // Use default dimensions if invalid
    if (docWidth <= 0.0 || docHeight <= 0.0) {
        docWidth = 1404.0;  // Default width
        docHeight = 1872.0; // Default height
    }
    
    // Calculate render dimensions
    int renderWidth = static_cast<int>(std::llround(docWidth));
    int renderHeight = static_cast<int>(std::llround(docHeight));
    
    if (isLandscape) {
        std::swap(renderWidth, renderHeight);
    }
    
    // Set up rendering parameters
    RenderParams params;
    setupRenderParams(params, document, renderWidth, renderHeight, isLandscape);
    
    // Create initial render
    QImage renderImage;
    renderDocument(document, params, renderImage);
    
    // Scale to target size
    outputImage = renderImage.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
}

void ThumbnailGenerator::generateBackgroundThumbnail(DocumentContext* context,
                                                   Document* document,
                                                   unsigned int pageNumber,
                                                   const QSize& targetSize,
                                                   QImage& outputImage)
{
    int pageIndex = getPageIndex(context, pageNumber);
    if (pageIndex < 0) {
        return; // Invalid page
    }
    
    // Get page dimensions from renderer
    DocumentRenderer* renderer = context->renderer;
    QSizeF pageSize = renderer->getPageSize(pageIndex);
    
    // Calculate scaled size maintaining aspect ratio
    QSize scaledSize = QSize(
        static_cast<int>(std::llround(pageSize.width())),
        static_cast<int>(std::llround(pageSize.height()))
    ).scaled(targetSize, Qt::KeepAspectRatio);
    
    // Render the page
    QImage pageImage = renderer->renderPage(pageIndex, scaledSize);
    
    if (!pageImage.isNull()) {
        outputImage = pageImage;
    }
}

void ThumbnailGenerator::saveThumbnailToFile(DocumentContext* context,
                                           const QImage& image,
                                           unsigned int pageNumber,
                                           const QString& outputPath)
{
    FileManager* fileManager = getGlobalFileManager();
    Logger* logger = getGlobalLogger();
    
    // Create temporary filename
    QString tempPath = QString("%1.tmp.png").arg(outputPath);
    
    // Ensure directory exists
    QString directory = fileManager->getDirectory(outputPath);
    if (!fileManager->createDirectory(directory)) {
        if (logger->isWarningEnabled()) {
            logger->warning("Failed to create thumbnail directory %s for page=%d",
                          directory.toLocal8Bit().constData(), pageNumber);
        }
        return;
    }
    
    // Save image to temporary file
    if (!image.save(tempPath, "PNG")) {
        if (logger->isWarningEnabled()) {
            logger->warning("Failed to write temporary thumbnail %s for page=%d",
                          tempPath.toLocal8Bit().constData(), pageNumber);
        }
        return;
    }
    
    // Move temporary file to final location
    if (fileManager->moveFile(tempPath, outputPath)) {
        // Notify thumbnail generation completion
        emit thumbnailGenerated(pageNumber);
    } else {
        if (logger->isWarningEnabled()) {
            logger->warning("Failed to move thumbnail into place (%s -> %s), page=%d",
                          tempPath.toLocal8Bit().constData(),
                          outputPath.toLocal8Bit().constData(),
                          pageNumber);
        }
    }
}

void ThumbnailGenerator::setupRenderParams(RenderParams& params,
                                         Document* document,
                                         int width, int height,
                                         bool isLandscape)
{
    // Initialize transform matrix
    params.transform.reset();
    params.format = QImage::Format_RGB32;
    params.backgroundColor = QColor(255, 255, 255); // White
    params.antialiasing = true;
    params.size = QSize(width, height);
    
    // Calculate document bounds
    QRectF docBounds;
    if (!isLandscape && document->hasRotation) {
        docBounds = getRotatedBounds(document);
    } else {
        docBounds = getStandardBounds(document);
        if (document->hasClipping) {
            docBounds.setWidth(document->clipWidth);
            docBounds.setHeight(document->clipHeight);
        }
    }
    
    // Calculate scaling to fit thumbnail
    double scaleX = static_cast<double>(width) / docBounds.width();
    double scaleY = static_cast<double>(height) / docBounds.height();
    double scale = std::min(scaleX, scaleY);
    
    // Center the content
    double offsetX = (width - scale * docBounds.width()) * 0.5;
    double offsetY = (height - scale * docBounds.height()) * 0.5;
    
    if (offsetX < 0.0) offsetX = 0.0;
    if (offsetY < 0.0) offsetY = 0.0;
    
    // Apply transformations
    params.transform.translate(width * 0.5, offsetY);
    params.transform.scale(scale, scale);
}

We can now see the complete implementation of thumbnail generation:

Initial validation and setup process

The function begins with thread-safe validation and mode determination:

// Thread-safe access to document data
pthread_mutex_t* contextMutex = reinterpret_cast<pthread_mutex_t*>(context + 200);
pthread_mutex_lock(contextMutex);
uint64_t pageInfo = getPageInfo(context + 248, pageNumber);  // Get existing page data
pthread_mutex_unlock(contextMutex);

// Determine if this is a background or scene thumbnail
bool isScenePage = true;
if (!pageInfo) {  // No existing thumbnail data
    isScenePage = false;
    // Check if document has valid dimensions for background rendering
    if (document->width > 0.0) {
        isScenePage = document->height > 0.0;
    }
}

// Determine orientation (landscape vs portrait) 
DocumentRenderer* renderer = context->renderer;
double aspectRatio = renderer->getPageAspectRatio(pageNumber);
bool isLandscapeOrientation = (aspectRatio > 1.0);  // Compare width vs height

Rendering engine: two generation modes

There are two distinct modes for thumbnail generation:

Scene Mode: interactive content rendering

This mode uses the document’s actual page dimensions with proper scaling and positioning to create detailed thumbnails with the correct aspect ratio.

// Get page dimensions (with fallback to default 1404x1872)
double docWidth, docHeight;
if (document->hasCustomDimensions) {
    docWidth = document->customWidth;
    docHeight = document->customHeight;
} else {
    docWidth = document->standardWidth; 
    docHeight = document->standardHeight;
}

if (docWidth <= 0.0 || docHeight <= 0.0) {
    docHeight = 1872.0;  // Default height
    docWidth = 1404.0;   // Default width
} else {
    docWidth = std::llround(docWidth);   // Width
    docHeight = std::llround(docHeight); // Height
}

// Calculate scaling and positioning
double scale = static_cast<double>(targetWidth) / docBounds.width();  // Scale factor
double centerOffset = (static_cast<double>(targetHeight) - scale * docBounds.height()) * 0.5;  // Center offset

// Apply transforms
QTransform transform;
transform.translate(static_cast<double>(targetWidth) * 0.5, centerOffset);
transform.scale(scale, scale);

// Render the page content with transforms
renderDocument(document, pageNumber, transform, renderImage);

// Scale to final thumbnail size with high-quality scaling
QImage thumbnail = renderImage.scaled(targetSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
Background Mode: template-based rendering

This mode renders directly from the PDF/document backend by getting the page size and rendering at thumbnail resolution:

// Get page size from document renderer
QSizeF pageSize = renderer->getPageSize(pageIndex);
int pageWidth = static_cast<int>(std::llround(pageSize.width()));
int pageHeight = static_cast<int>(std::llround(pageSize.height()));

// Scale page size to thumbnail dimensions
QSize scaledSize = QSize(pageWidth, pageHeight).scaled(targetSize, Qt::KeepAspectRatio);

// Render page directly from document backend at scaled size
QImage thumbnail = renderer->renderPage(pageIndex, scaledSize, 
                                      0.0, 0.0,  // x, y offset
                                      scaledSize.width(), scaledSize.height());

Error recovery and fallback mechanisms

It ensures that a valid thumbnail is always created, even if the rendering process fails.

if (thumbnail.isNull()) {
    // Create blank thumbnail if generation failed
    thumbnail = QImage(targetSize, QImage::Format_RGB32);  // Create image with target size
    thumbnail.fill(QColor(255, 255, 255));  // Fill with white background
}

File storage and caching strategy

Finally, it implements atomic file operations to prevent corruption.

FileManager* fileManager = getGlobalFileManager();

// Generate temporary filename: "{outputPath}.tmp.png"
QString tempPath = QString("%1.tmp.png").arg(outputPath);

// Create directory if needed
QString directory = fileManager->getDirectory(outputPath);
if (!fileManager->createDirectory(directory)) {
    // Handle directory creation error
    return;
}

// Check if directory creation succeeded
if (fileManager->directoryExists(directory)) {
    // Write thumbnail as PNG to temporary file
    if (thumbnail.save(tempPath, "PNG")) {
        // Atomically move temp file to final location
        if (fileManager->moveFile(tempPath, outputPath)) {
            // Signal completion
            emit thumbnailGenerated(pageNumber);
        }
    }
}

Error handling and edge cases

The implementation includes detailed error checking and appropriate logging:

  • Storage space check: Not enough space to store thumbnails, page=%d
  • Document lock check: Clients should not request thumbs for a locked document, page=%d
  • Directory creation failure: Failed to create thumbnail directory %s for page %d
  • Write failure: Failed to write temporary thumbnail %s for page %d
  • Move failure: Failed to move thumbnail into place (%s -> %s), page=%d

The renderDocument() function (which corresponds to the original sub_8FE030 in the raw pseudo code) handles the actual page rendering with the calculated transforms. Meanwhile, the main function orchestrates the entire thumbnail generation pipeline, ensuring proper error handling and file management. This can be confirmed by checking the functions that call this one:

  • sub_8F6BD0: handles document loading, processing and callbacks (doLoadDocument).
  • sub_8FEA30: generate thumbnail (doGenerateThumbnail)
  • sub_9036C0: document exporter (doExportDocument).

Based on the call site in the thumbnail generation code:

// Original disassembled call:
sub_8FE030(v73, a1, a2, a3, &v84, 0, 0);

// Mapped parameters:
renderDocument(
    output_image,     // v73 - QImage output buffer
    context,          // a1  - document context/renderer  
    page_data,        // a2  - page information structure
    page_number,      // a3  - page index to render
    transform,        // &v84 - QTransform for coordinate mapping
    render_flags,     // 0   - rendering options/flags
    coord_flags       // 0   - coordinate system flags
);

The function signature would therefore be something like this:

void __fastcall renderDocument(
    QImage* outputImage,              // Destination image buffer
    DocumentContext* context,         // Document renderer and state
    PageData** pageInfo,              // Page-specific data and metadata
    unsigned int pageNumber,          // Zero-based page index
    const QTransform* transform,      // Coordinate transformation matrix
    RenderFlags renderOptions,        // Rendering quality/mode flags
    CoordinateFlags coordFlags        // Coordinate system options
);

Here is the raw implementation:

See the raw pseudo code
QImage *__usercall sub_8FE030(
        _QWORD *a1,
        __int64 *a2,
        unsigned int a3,
        __int64 a4,
        unsigned __int8 a5,
        __int64 a6,
        QImage *a7)
{
  // variable declarations ...

  v18 = a5;
  sub_90E090(&v123, a2);
  v19 = v123;
  sub_47BC70(&v124);
  if ( v19 == 8 )
  {
    v20 = *(unsigned int *)(a4 + 88);
    if ( (_DWORD)v20 != 1 && (_DWORD)v20 != 7 && (v20 & 0xFFFFFFFD) != 4 )
    {
      v45 = sub_8EAE00();
      v20 = 6;
      if ( *((_BYTE *)v45 + 17) )
      {
        v46 = (_QWORD *)v45[1];
        v123 = 2;
        v124 = 0;
        v125 = 0;
        v126 = v46;
        QMessageLogger::warning(&v119, (QMessageLogger *)&v123);
        v47 = sub_4DC040(&v119, "invalid image format in tile request");
        v48 = *(unsigned int *)(a4 + 88);
        v49 = *(_DWORD *)(*(_QWORD *)v47 + 40LL);
        *(_QWORD *)&v116 = *(_QWORD *)v47;
        *(_DWORD *)(v116 + 40) = v49 + 1;
        qt_QMetaEnum_debugOperator(v114, (QDebug *)&v116, v48, (const QMetaObject *)&QImage::staticMetaObject, "Format");
        QDebug::~QDebug((QDebug *)v114);
        QDebug::~QDebug((QDebug *)&v116);
        QDebug::~QDebug((QDebug *)&v119);
        v20 = 6;
      }
    }
    QImage::QImage(v114, a4 + 80, v20);
    v22 = sub_8EAE00();
    if ( *((_BYTE *)v22 + 16) )
    {
      v50 = (_QWORD *)v22[1];
      v123 = 2;
      v124 = 0;
      v125 = 0;
      v126 = v50;
      QMessageLogger::debug(&v113, (QMessageLogger *)&v123);
      v51 = sub_4DC040(&v113, " -> renderToImage:");
      v112 = *(_QWORD *)sub_B74760(v51, a3);
      ++*(_DWORD *)(v112 + 40);
      operator<<(&v111, &v112, a4 + 80);
      qt_QMetaEnum_debugOperator(
        &v110,
        (QDebug *)&v111,
        *(unsigned int *)(a4 + 88),
        (const QMetaObject *)&QImage::staticMetaObject,
        "Format");
      v52 = "aa";
      if ( !*(_BYTE *)(a4 + 144) )
        v52 = "noaa";
      v53 = sub_4DC040(&v110, v52);
      v54 = "selected";
      if ( !*(_BYTE *)(a4 + 145) )
        v54 = "";
      v55 = sub_4DC040(v53, v54);
      v56 = "unselected";
      if ( !*(_BYTE *)(a4 + 146) )
        v56 = "";
      v57 = sub_4DC040(v55, v56);
      v58 = sub_4DC040(v57, "bg-filter:");
      v59 = *(unsigned __int8 *)(a4 + 136);
      v119 = 0;
      v120 = L"n/a";
      v60 = v58;
      v121 = 3;
      if ( v59 )
      {
        v67 = *(_OWORD *)(a4 + 112);
        v117 = *(_QWORD *)(a4 + 128);
        v116 = v67;
        if ( (_QWORD)v67 )
          sub_DDC440(1u, (atomic_uint *)v67);
      }
      else
      {
        *(_QWORD *)&v116 = 0;
        *((_QWORD *)&v116 + 1) = L"n/a";
        v117 = 3;
        v120 = 0;
        v121 = 0;
      }
      v109 = *(_QWORD *)sub_B747C0(v60, &v116);
      ++*(_DWORD *)(v109 + 40);
      operator<<(&v108, &v109, a4);
      QDebug::~QDebug((QDebug *)&v108);
      QDebug::~QDebug((QDebug *)&v109);
      sub_47BC70(&v116);
      sub_47BC70(&v119);
      QDebug::~QDebug((QDebug *)&v110);
      QDebug::~QDebug((QDebug *)&v111);
      QDebug::~QDebug((QDebug *)&v112);
      QDebug::~QDebug((QDebug *)&v113);
    }
    QImage::QImage((QImage *)&v123);
    v126 = 0;
    v127 = 0x1010101000100LL;
    *(_WORD *)&v131[24] &= 0xFC00u;
    *(_QWORD *)&v131[16] = 0x3FF0000000000000LL;
    v128 = xmmword_1303AB0;
    v129 = xmmword_1303AC0;
    v130 = xmmword_1303AD0;
    *(_OWORD *)v131 = xmmword_1303AE0;
    QColor::QColor(v132, 3);
    v138 = 0;
    v135 = xmmword_1378260;
    v133 = 0u;
    v134 = 0u;
    v136 = 0u;
    v139 = xmmword_E1F060;
    v140 = 0;
    memset(v141, 0, sizeof(v141));
    QImage::QImage((QImage *)v142);
    v126 = v114;
    v143 = 0;
    v144 = 0;
    *(_QWORD *)&v23 = QImage::rect((QImage *)v114);
    v24 = *(_OWORD *)(a4 + 32);
    v25 = *(_OWORD *)(a4 + 48);
    *((_QWORD *)&v23 + 1) = v26;
    v27 = *(_OWORD *)a4;
    v28 = *(_OWORD *)(a4 + 16);
    v135 = v23;
    v29 = *(_OWORD *)(a4 + 58);
    v130 = v24;
    *(_OWORD *)v131 = v25;
    v128 = v27;
    v129 = v28;
    *(_OWORD *)&v131[10] = v29;
    if ( v138 )
    {
      v64 = (_OWORD *)(v137 + 112);
      v65 = *(QPainter **)v137;
      *(_OWORD *)(v137 + 112) = v27;
      v64[1] = v28;
      v66 = *(_OWORD *)(a4 + 48);
      v64[2] = *(_OWORD *)(a4 + 32);
      v64[3] = v66;
      *(_OWORD *)((char *)v64 + 58) = *(_OWORD *)(a4 + 58);
      if ( v65 )
        QPainter::setTransform(v65, (const QTransform *)a4, 0);
    }
    v30 = *(_DWORD *)(a4 + 92);
    v31 = *(_WORD *)(a4 + 145);
    v32 = a1[126];
    LOBYTE(v127) = *(_BYTE *)(a4 + 144);
    WORD1(v127) = v31;
    v140 = v32;
    v144 = v30;
    sub_47F330(v141, a1 + 127);
    *(_QWORD *)v132 = *(_QWORD *)(a4 + 96);
    v38 = *(unsigned __int8 *)(a4 + 148);
    *(_QWORD *)&v132[6] = *(_QWORD *)(a4 + 102);
    v39 = *(unsigned __int8 *)(a4 + 147);
    v40 = *(_BYTE *)(a4 + 149);
    BYTE4(v127) = *(_BYTE *)(a4 + 147);
    HIBYTE(v127) = v40;
    if ( !v38 )
    {
      v41 = *a2;
      goto LABEL_11;
    }
    v41 = *a2;
    v61 = *(unsigned __int8 *)(*a2 + 288);
    if ( !*(_BYTE *)(*a2 + 288) )
    {
LABEL_11:
      if ( !*(_BYTE *)(a4 + 146) )
      {
        *(_DWORD *)v132 = 1;
        *(_QWORD *)&v132[4] = 0;
        *(_WORD *)&v132[12] = 0;
      }
      goto LABEL_13;
    }
    if ( v18 )
      goto LABEL_32;
    sub_A1D910(&v116);
    if ( (unsigned __int8)sub_47BBA0(a1 + 6, &v116) )
    {
      sub_47BC70(&v116);
    }
    else
    {
      v119 = 0;
      v120 = L"epub";
      v121 = 4;
      v81 = (unsigned __int8)sub_47BBA0(a1 + 6, &v119);
      sub_47BC70(&v119);
      sub_47BC70(&v116);
      if ( !v81 )
      {
LABEL_32:
        v61 = 0;
        goto LABEL_33;
      }
    }
    if ( a1[196] )
    {
      v79 = (unsigned __int8 (*)(void))a1[197];
      LOBYTE(v119) = 1;
      v61 = v79();
      if ( !v61 )
      {
        v80 = sub_8EAE00();
        if ( *((_BYTE *)v80 + 17) )
        {
          v86 = v80[1];
          v119 = 2;
          v120 = 0;
          v121 = 0;
          v122 = v86;
          QMessageLogger::warning(&v116, (QMessageLogger *)&v119);
          sub_4DC040(&v116, "failed to acquire template background lock mid-render");
          QDebug::~QDebug((QDebug *)&v116);
        }
        QImage::QImage(a7);
        goto LABEL_14;
      }
    }
LABEL_33:
    v62 = sub_8F10E0(a1, a3);
    if ( (v62 & 0x80000000) != 0 )
    {
      if ( v61 )
        goto LABEL_35;
LABEL_37:
      v41 = *a2;
LABEL_13:
      sub_972B50(
        v41,
        (int)&v123,
        v39,
        v33,
        v34,
        v35,
        v36,
        v37,
        v88,
        v89,
        v90,
        v91,
        v92,
        v93,
        v94,
        v95,
        v96,
        v97,
        v98,
        v99,
        v100,
        v101,
        v102,
        v103,
        v104,
        v105,
        v106.n128_i8[0]);
      QPaintDevice::QPaintDevice(a7);
      v42 = v115;
      *(_QWORD *)a7 = &unk_15AAB10;
      *((_QWORD *)a7 + 2) = v42;
      v115 = 0;
LABEL_14:
      sub_6244B0(&v123);
      QImage::~QImage((QImage *)v114);
      return a7;
    }
    v68 = a1[133];
    v100 = v7.n128_u32[0];
    v101 = v8;
    v102 = v9;
    v103 = v10;
    v104 = v11;
    v69 = v68 + 144;
    if ( *(_BYTE *)(v68 + 168) )
    {
      if ( *(_BYTE *)(a4 + 136) )
      {
        QString::operator=(v68 + 144, a4 + 112);
      }
      else
      {
        *(_BYTE *)(v68 + 168) = 0;
        sub_47BC70(v68 + 144);
      }
      v68 = a1[133];
    }
    else if ( *(_BYTE *)(a4 + 136) )
    {
      v82 = a1[133];
      v83 = *(_QWORD *)(a4 + 128);
      v84 = *(atomic_uint **)(a4 + 112);
      *(_OWORD *)(v68 + 144) = *(_OWORD *)(a4 + 112);
      *(_QWORD *)(v68 + 160) = v83;
      if ( v84 )
      {
        sub_DDC440(1u, v84);
        v82 = a1[133];
      }
      v68 = v82;
      *(_BYTE *)(v69 + 24) = 1;
    }
    v70 = *(double *)a4;
    v71 = *(double *)(a4 + 32);
    v72 = *(_QWORD *)(a4 + 80);
    v73 = (*(double (__fastcall **)(__int64, _QWORD))(*(_QWORD *)v68 + 192LL))(v68, v62);
    v75 = v74;
    v119 = 0;
    v120 = 0;
    v7.n128_f64[0] = v73 * 0.5 * v70;
    v107.n128_u64[0] = QTransform::map(a4);
    v107.n128_u64[1] = v76;
    v106 = vsubq_f64(v7, v107);
    (*(void (__fastcall **)(__int128 *__return_ptr, _QWORD, _QWORD, __int64, double, double, double, double))(*(_QWORD *)a1[133] + 120LL))(
      &v116,
      a1[133],
      v62,
      v72,
      v106.n128_f64[0],
      v106.n128_f64[1],
      v73 * v70,
      v71 * v75);
    v77 = v118;
    if ( v118 )
    {
      if ( a6 )
        v85 = v118 == 1;
      else
        v85 = 0;
      if ( v85 )
      {
        LOBYTE(v77) = 1;
        *(_DWORD *)a6 = v116;
        *(_BYTE *)(a6 + 4) = 1;
        goto LABEL_49;
      }
    }
    else
    {
      QImage::operator=(v142, &v116);
      v77 = v118;
      v143 = 1;
    }
    if ( v77 == 255 )
    {
LABEL_50:
      v78 = a1[133];
      LOBYTE(v122) = 0;
      if ( *(_BYTE *)(v78 + 168) && (*(_BYTE *)(v78 + 168) = 0, sub_47BC70(v78 + 144), (_BYTE)v122) )
      {
        LOBYTE(v122) = 0;
        sub_47BC70(&v119);
        if ( !v61 )
          goto LABEL_37;
      }
      else if ( !v61 )
      {
        goto LABEL_37;
      }
LABEL_35:
      if ( a1[196] )
      {
        v63 = (void (*)(void))a1[197];
        LOBYTE(v116) = 0;
        v63();
      }
      goto LABEL_37;
    }
LABEL_49:
    ((void (__fastcall *)(__int64 *, __int128 *))`vtable for'std::_Sp_counted_ptr_inplace<Page,std::allocator<Page>,(__gnu_cxx::_Lock_policy)2>[(char)v77 + 59])(
      &v119,
      &v116);
    goto LABEL_50;
  }
  v44 = sub_8EAE00();
  if ( *((_BYTE *)v44 + 17) )
  {
    v87 = (_QWORD *)v44[1];
    v123 = 2;
    v124 = 0;
    v125 = 0;
    v126 = v87;
    QMessageLogger::warning((QMessageLogger *)&v123, " -> page not in ready status...");
  }
  QImage::QImage(a7);
  return a7;
}

This function takes a set of rendering parameters and produces a QImage* result. It is a render-to-image pipeline that validates the input parameters, sets up a QImage with the correct format, applies transforms and options, and delegates the actual tile rendering to another (another..) lower-level routine. Before calling the renderer, the function does the following:

  • Validates the image format and logs warnings if it’s not a supported format
  • Constructs temporary QImage instances to prepare render targets
  • Reads transform matrices from a4 (the 0–64 bytes contain a QTransform)
  • If painter state is active the transform is applied via QPainter::setTransform
  • Extracts flags from a4 + 144 to 149, such as anti-aliasing, selection and background filtering.
  • Prepares colour, rectangle and other painting states (QColor, QTransform, QPaintDevice)

Then it calls sub_8F10E0:

v62 = sub_8F10E0(a1, a3);

a1 is probably a rendering context or document object, a3 is a tile/page index (tile request ID) and v62 is a status/result code. After sub_8F10E0 returns, the rest of the function involves applying transformations and scaling (via QTransform::map) and blitting the result into a7.

Therefore, this program uses tiled rendering for performance reasons, breaking large content into smaller tiles that can be rendered independently and cached independently. sub_8F10E0 is the tile renderer’s entry point. It decides whether the requested tile/page (a3) can be rendered and returns a tile index/status to be used by the document/page renderer. If the result is negative, the process is aborted; if positive, scaling and rasterising of that tile into a QImage proceed.

Hidden API endpoints discovery

Back to sub_56EDC0 (now renamed HttpRequestRouter)! I found two new, undocumented routes, or at least some community-made documentation.

Undocumented thumbnail API

The first one is /thumbnail, which takes a document ID and returns the .thumbnail of the last page opened.

curl 'http://10.11.99.1/thumbnail/{guid}'

The endpoint returns a 384x512 image as a png like shown earlier. I’m not sure if the resolution can be queried or if it is fixed.

Experimental search functionality

The second one is a /search function. It takes a keyword and returns a JSON of titles that match (in theory) the keyword.

curl 'http://10.11.99.1/search/{keyword}'

The output is a json array of documents:

[
    {
        "Bookmarked": false,
        "CurrentPage": 3,
        "ID": "63b02eef-b19f-4238-83b9-861a0492e22f",
        "ModifiedClient": "2025-08-18T09:11:49.462Z",
        "Parent": "",
        "Type": "DocumentType",
        "VissibleName": "Planning",
        "fileType": "notebook"
    }
]

You may also notice the typo in the response: typo.png

These two functions have not yet been implemented in the front end of the app and are probably still in development. The search function always returns the same output: the base directory: youshoudlntcatchthisstring.png

Once this feature has been properly implemented, a Violentmonkey script will probably be useful for implementing it. Here is a POC: search-bar.png

I could implement the thumbnail feature in the same way, but the front end uses React and it’s difficult to inject code into it. I’m not a developer at reMarkable, so it shouldn’t be my job to implement that feature nor trying to understand how thumbnails are generated..