aboutsummaryrefslogtreecommitdiff
path: root/qtmips_gui/qhtml5file_html5.cpp
blob: 60f26d72e3aed79693f4e98d38163e42cefec520 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtCore module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qhtml5file.h"

#include <qdebug.h>

#include <emscripten.h>
#include <emscripten/html5.h>

//
// This file implements file load via HTML file input element and file save via browser download.
//

// Global user file data ready callback and C helper function. JavaScript will
// call this function when the file data is ready; the helper then forwards
// the call to the current handler function. This means there can be only one
// file open in proress at a given time.
std::function<void(char *, size_t, const char *)> g_qtFileDataReadyCallback;
extern "C" EMSCRIPTEN_KEEPALIVE void qt_callFileDataReady(char *content, size_t contentSize, const char *fileName)
{
    if (g_qtFileDataReadyCallback == nullptr)
        return;

    g_qtFileDataReadyCallback(content, contentSize, fileName);
    g_qtFileDataReadyCallback = nullptr;
}

namespace {
    void loadFile(const char *accept, std::function<void(char *, size_t, const char *)> fileDataReady)
    {
        if (::g_qtFileDataReadyCallback)
            puts("Warning: Concurrent loadFile() calls are not supported. Cancelling earlier call");

        // Call qt_callFileDataReady to make sure the emscripten linker does not
        // optimize it away, which may happen if the function is called from JavaScript
        // only. Set g_qtFileDataReadyCallback to null to make it a a no-op.
        ::g_qtFileDataReadyCallback = nullptr;
        ::qt_callFileDataReady(nullptr, 0, nullptr);

        ::g_qtFileDataReadyCallback = fileDataReady;
        EM_ASM_({
            const accept = UTF8ToString($0);

            // Crate file file input which whil display the native file dialog
            var fileElement = document.createElement("input");
            document.body.appendChild(fileElement);
            fileElement.type = "file";
            fileElement.style = "display:none";
            fileElement.accept = accept;
            fileElement.onchange = function(event) {
                const files = event.target.files;

                // Read files
                for (var i = 0; i < files.length; i++) {
                    const file = files[i];
                    var reader = new FileReader();
                    reader.onload = function() {
                        const name = file.name;
                        var contentArray = new Uint8Array(reader.result);
                        const contentSize = reader.result.byteLength;

                        // Copy the file file content to the C++ heap.
                        // Note: this could be simplified by passing the content as an
                        // "array" type to ccall and then let it copy to C++ memory.
                        // However, this built-in solution does not handle files larger
                        // than ~15M (Chrome). Instead, allocate memory manually and
                        // pass a pointer to the C++ side (which will free() it when done).

                        // TODO: consider slice()ing the file to read it picewise and
                        // then assembling it in a QByteArray on the C++ side.

                        const heapPointer = _malloc(contentSize);
                        const heapBytes = new Uint8Array(Module.HEAPU8.buffer, heapPointer, contentSize);
                        heapBytes.set(contentArray);

                        // Null out the first data copy to enable GC
                        reader = null;
                        contentArray = null;

                        // Call the C++ file data ready callback
                        ccall("qt_callFileDataReady", null,
                            ["number", "number", "string"], [heapPointer, contentSize, name]);
                    };
                    reader.readAsArrayBuffer(file);
                }

                // Clean up document
                document.body.removeChild(fileElement);

            }; // onchange callback

            // Trigger file dialog open
            fileElement.click();

        }, accept);
    }

    void saveFile(const char *contentPointer, size_t contentLength, const char *fileNameHint)
    {
        EM_ASM_({
            // Make the file contents and file name hint accessible to Javascript: convert
            // the char * to a JavaScript string and create a subarray view into the C heap.
            const contentPointer = $0;
            const contentLength = $1;
            const fileNameHint = UTF8ToString($2);
            const fileContent = Module.HEAPU8.subarray(contentPointer, contentPointer + contentLength);

            // Create a hidden download link and click it programatically
            const fileblob = new Blob([fileContent], { type : "application/octet-stream" } );
            var link = document.createElement("a");
            document.body.appendChild(link);
            link.download = fileNameHint;
            link.href = window.URL.createObjectURL(fileblob);
            link.style = "display:none";
            link.click();
            document.body.removeChild(link);

        }, contentPointer, contentLength, fileNameHint);
    }
}

/*!
    \brief Read local file via file dialog.

    Call this function to make the browser display an open-file dialog. This function
    returns immediately, and \a fileDataReady is called when the user has selected a file
    and the file contents has been read.

    \a The accept argument specifies which file types to accept, and must follow the
    <input type="file"> html standard formatting, for example ".png, .jpg, .jpeg".

    This function is implemented on Qt for WebAssembly only. A nonfunctional cross-
    platform stub is provided so that code that uses it can compile on all platforms.
*/
void QHtml5File::load(const QString &accept, std::function<void(const QByteArray &, const QString &)> fileDataReady)
{
    loadFile(accept.toUtf8().constData(), [=](char *content, size_t size, const char *fileName) {

        // Copy file data into QByteArray and free buffer that was allocated
        // on the JavaScript side. We could have used QByteArray::fromRawData()
        // to avoid the copy here, but that would make memory management awkward.
        QByteArray qtFileContent(content, size);
        free(content);

        // Call user-supplied data ready callback
        fileDataReady(qtFileContent, QString::fromUtf8(fileName));
    });
}

/*!
    \brief Write local file via browser download

    Call this function to make the browser start a file download. The file
    will contains the given \a content, with a suggested \a fileNameHint.

    This function is implemented on Qt for WebAssembly only. A nonfunctional cross-
    platform stub is provided so that code that uses it can compile on all platforms.
*/
void QHtml5File::save(const QByteArray &content, const QString &fileNameHint)
{
    // Convert to C types and save
    saveFile(content.constData(), content.size(), fileNameHint.toUtf8().constData());
}