Add plugin APIs for ebook db
[platform/core/multimedia/libmedia-service.git] / plugin / media-ebook-plugin.cpp
1 /*
2  * libmedia-service
3  *
4  * Copyright (c) 2021 Samsung Electronics Co., Ltd. All rights reserved.
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  *
18  */
19 #include <podofo/podofo.h>
20 #include <stack>
21 #include <memory>
22 #include <dlog.h>
23 #include <glib.h>
24 #include <stdbool.h>
25
26 #include <zip.h>
27 #include <libxml/xmlmemory.h>
28 #include <libxml/parser.h>
29 #include <libxml/HTMLparser.h>
30
31 #include <regex>
32 #include <iterator>
33 #include <sqlite3.h>
34
35 #ifdef LOG_TAG
36 #undef LOG_TAG
37 #endif
38
39 #define LOG_TAG "MEDIA_SERVICE"
40 #define INSERT_QUERY "INSERT INTO words(file_id, word) SELECT id, ? FROM files WHERE path=? ON CONFLICT (file_id, word) DO UPDATE SET frequency=frequency+1;"
41 #define TOKEN_KEY "\\s+"
42 #define SPECIAL_CHAR "[\\{\\}\\[\\]\\/?.,;:|\\)*~`!^\\-_+<>@\\#$%&\\\\=\\(\\\'\\\"]"
43
44 class TextFinderInterface
45 {
46 public:
47         virtual ~TextFinderInterface() = default;
48         virtual bool find(const char *keyword) = 0;
49         virtual void insert() = 0;
50 };
51
52 class TextFinder : public TextFinderInterface
53 {
54 public:
55         virtual ~TextFinder() = default;
56         bool match(std::string& text, const char *keyword);
57         void batchInsert(std::string& text);
58
59         sqlite3 *dbHandle {};
60         const char *filePath {};
61 };
62
63 bool TextFinder::match(std::string& text, const char *keyword)
64 {
65         if (!keyword)
66                 return false;
67
68         if (text.empty())
69                 return false;
70
71         std::regex re(keyword, std::regex::icase);
72
73         if (std::regex_search(text, re)) {
74                 LOGD("Found [%s]", keyword);
75                 return true;
76         }
77
78         return false;
79 }
80
81 void TextFinder::batchInsert(std::string& text)
82 {
83         if (!dbHandle || !filePath || text.empty())
84                 return;
85
86         sqlite3_stmt *stmt = NULL;
87         const std::regex sp(SPECIAL_CHAR);
88         std::string temp = std::regex_replace(text, sp, "");
89         bool isTransaction = false;
90
91         if (sqlite3_exec(dbHandle, "BEGIN;", NULL, NULL, NULL) == SQLITE_OK)
92                 isTransaction = true;
93
94         sqlite3_prepare_v2(dbHandle, INSERT_QUERY, -1, &stmt, NULL);
95
96         const std::regex re(TOKEN_KEY);
97         std::sregex_token_iterator end;
98
99         for (std::sregex_token_iterator i(temp.begin(), temp.end(), re, -1); i != end; ++i) {
100                 sqlite3_bind_text(stmt, 1, (*i).str().c_str(), -1, SQLITE_TRANSIENT);
101                 sqlite3_bind_text(stmt, 2, filePath, -1, SQLITE_TRANSIENT);
102                 sqlite3_step(stmt);
103                 sqlite3_reset(stmt);
104         }
105
106         sqlite3_finalize(stmt);
107
108         if (isTransaction)
109                 sqlite3_exec(dbHandle, "COMMIT;", NULL, NULL, NULL);
110 }
111
112 /*---------------- PDF -----------------------*/
113 class PdfTextFinder : public TextFinder
114 {
115 public:
116         explicit PdfTextFinder(const char *path);
117         PdfTextFinder(sqlite3 *handle, const char *path);
118         bool find(const char *keyword) override;
119         void insert() override;
120
121 private:
122         std::string parseTextFromPage(unsigned int index);
123         bool loaded {};
124
125         PoDoFo::PdfMemDocument pdf {};
126 };
127
128 PdfTextFinder::PdfTextFinder(const char *path)
129 {
130         if (!path) {
131                 LOGE("invalid path");
132                 return;
133         }
134
135         LOGD("%s", path);
136
137         try {
138                 pdf.Load(path);
139                 loaded = true;
140         } catch (const PoDoFo::PdfError& e) {
141                 LOGE("Initialization failed : %s", e.what());
142         }
143 }
144
145 PdfTextFinder::PdfTextFinder(sqlite3 *handle, const char *path)
146 {
147         if (!handle) {
148                 LOGE("invalid handle");
149                 return;
150         }
151
152         if (!path) {
153                 LOGE("invalid path");
154                 return;
155         }
156
157         LOGD("%s", path);
158
159         try {
160                 pdf.Load(path);
161                 loaded = true;
162                 dbHandle = handle;
163                 filePath = path;
164         } catch (const PoDoFo::PdfError& e) {
165                 LOGE("Initialization failed : %s", e.what());
166         }
167 }
168
169 bool PdfTextFinder::find(const char *keyword)
170 {
171         if (!loaded)
172                 return false;
173
174         if (!keyword) {
175                 LOGE("Invalid keyword");
176                 return false;
177         }
178
179         for (int n = 0; n < pdf.GetPageCount(); ++n) {
180                 auto text = parseTextFromPage(n);
181                 if (match(text, keyword))
182                         return true;
183         }
184
185         return false;
186 }
187
188 void PdfTextFinder::insert()
189 {
190         if (!loaded)
191                 return;
192
193         for (int n = 0; n < pdf.GetPageCount(); ++n) {
194                 auto text = parseTextFromPage(n);
195                 batchInsert(text);
196         }
197 }
198
199 std::string PdfTextFinder::parseTextFromPage(unsigned int index)
200 {
201         std::string fullText;
202
203         PoDoFo::EPdfContentsType type;
204         PoDoFo::PdfVariant var;
205         PoDoFo::PdfFont *cur_font = NULL;
206         bool text_block = false;
207         const char *tok;
208         std::stack<PoDoFo::PdfVariant> stack;
209         PoDoFo::PdfString unicode;
210         PoDoFo::PdfArray array;
211
212         PoDoFo::PdfPage* page = pdf.GetPage(index);
213         if (!page)
214                 return fullText;
215
216         PoDoFo::PdfContentsTokenizer tokenizer(page);
217
218         while (tokenizer.ReadNext(type, tok, var)) {
219                 if (type != PoDoFo::ePdfContentsType_Keyword) {
220                         if (text_block)
221                                 stack.push(var);
222
223                         continue;
224                 }
225
226                 if (!text_block && strcmp(tok, "BT") == 0) {
227                         text_block = true;
228                         continue;
229                 } else if (text_block && strcmp(tok, "ET") == 0) {
230                         text_block = false;
231                 }
232
233                 if (!text_block)
234                         continue;
235
236                 if (strcmp(tok, "Tf") == 0) {
237                         if (stack.size() < 2) {
238                                 cur_font = NULL;
239                                 continue;
240                         }
241
242                         stack.pop();
243                         cur_font = pdf.GetFont(page->GetFromResources(PoDoFo::PdfName("Font"), stack.top().GetName()));
244                 } else if (strcmp(tok, "Tj") == 0 || strcmp(tok, "'") == 0 || strcmp(tok, "\"") == 0) {
245                         if (stack.empty())
246                                 continue;
247
248                         if (!cur_font || !cur_font->GetEncoding())
249                                 continue;
250
251                         unicode = cur_font->GetEncoding()->ConvertToUnicode(stack.top().GetString(), cur_font);
252                         fullText += unicode.GetStringUtf8();
253
254                         stack.pop();
255                 } else if (strcmp(tok, "TJ") == 0) {
256                         if (stack.empty())
257                                 continue;
258
259                         array = stack.top().GetArray();
260                         stack.pop();
261
262                         for (int i = 0; i < static_cast<int>(array.GetSize()); i++) {
263                                 if (array[i].IsString() || array[i].IsHexString()) {
264                                         if (!cur_font || !cur_font->GetEncoding())
265                                                 continue;
266
267                                         unicode = cur_font->GetEncoding()->ConvertToUnicode(array[i].GetString(), cur_font);
268                                         fullText += unicode.GetStringUtf8();
269                                 }
270                         }
271                 }
272         }
273
274         return fullText;
275 }
276
277 /*---------------- EPUB -----------------------*/
278
279 class EpubTextFinder : public TextFinder
280 {
281 public:
282         explicit EpubTextFinder(const char *path);
283         EpubTextFinder(sqlite3 *handle, const char *path);
284         bool find(const char *keyword) override;
285         void insert() override;
286
287         ~EpubTextFinder() override;
288
289 private:
290         bool htmlNodeFindRecursive(xmlNodePtr node, const char *keyword);
291         void htmlNodeFindRecursiveForDb(xmlNodePtr node);
292         bool htmlFind(const char *html_buf, int buf_size, const char *keyword);
293         void htmlFindForDb(const char *html_buf, int buf_size);
294
295         zip_t *z {};
296 };
297
298 EpubTextFinder::EpubTextFinder(const char *path)
299 {
300         if (!path) {
301                 LOGE("invalid path");
302                 return;
303         }
304
305         LOGD("%s", path);
306
307         int err = 0;
308         z = zip_open(path, ZIP_RDONLY, &err);
309         if (err != 0)
310                 LOGE("zip_open failed");
311 }
312
313 EpubTextFinder::EpubTextFinder(sqlite3 *handle, const char *path)
314 {
315         if (!handle) {
316                 LOGE("invalid handle");
317                 return;
318         }
319
320         if (!path) {
321                 LOGE("invalid path");
322                 return;
323         }
324
325         LOGD("%s", path);
326
327         int err = 0;
328         z = zip_open(path, ZIP_RDONLY, &err);
329         if (err != 0)
330                 LOGE("zip_open failed");
331
332         dbHandle = handle;
333         filePath = path;
334 }
335
336 EpubTextFinder::~EpubTextFinder()
337 {
338         if (!z)
339                 return;
340
341         zip_close(z);
342         z = nullptr;
343 }
344
345 bool EpubTextFinder::find(const char *keyword)
346 {
347         zip_stat_t sb = {0, };
348
349         if (!keyword) {
350                 LOGE("Invalid keyword");
351                 return false;
352         }
353
354         int entry_len = zip_get_num_entries(z, ZIP_FL_UNCHANGED);
355         for (int i = 0; i < entry_len; i++) {
356                 if (!g_str_has_suffix(zip_get_name(z, i, ZIP_FL_ENC_GUESS), "html"))
357                         continue;
358
359                 if (zip_stat_index(z, i, 0, &sb) != 0)
360                         continue;
361
362                 zip_file_t *file = zip_fopen_index(z, i, 0);
363                 if (!file)
364                         continue;
365
366                 std::vector<char> file_buf(sb.size);
367
368                 zip_int64_t readn = zip_fread(file, file_buf.data(), sb.size);
369                 zip_fclose(file);
370
371                 if ((readn == static_cast<zip_int64_t>(sb.size)) &&
372                         htmlFind(file_buf.data(), sb.size, keyword))
373                         return true;
374         }
375
376         return false;
377 }
378
379 void EpubTextFinder::insert()
380 {
381         zip_stat_t sb = {0, };
382
383         int entry_len = zip_get_num_entries(z, ZIP_FL_UNCHANGED);
384         for (int i = 0; i < entry_len; i++) {
385                 if (!g_str_has_suffix(zip_get_name(z, i, ZIP_FL_ENC_GUESS), "html"))
386                         continue;
387
388                 if (zip_stat_index(z, i, 0, &sb) != 0)
389                         continue;
390
391                 zip_file_t *file = zip_fopen_index(z, i, 0);
392                 if (!file)
393                         continue;
394
395                 std::vector<char> file_buf(sb.size);
396
397                 zip_int64_t readn = zip_fread(file, file_buf.data(), sb.size);
398                 zip_fclose(file);
399
400                 if (readn == static_cast<zip_int64_t>(sb.size))
401                         htmlFindForDb(file_buf.data(), sb.size);
402         }
403 }
404
405 bool EpubTextFinder::htmlNodeFindRecursive(xmlNodePtr node, const char *keyword)
406 {
407         for (xmlNodePtr cur = node; cur; cur = cur->next) {
408                 if (cur->type == XML_TEXT_NODE) {
409                         std::string text(reinterpret_cast<char*>(cur->content));
410                         if (match(text, keyword))
411                                 return true;
412                 }
413
414                 if (htmlNodeFindRecursive(cur->children, keyword))
415                         return true;
416         }
417
418         return false;
419 }
420
421 void EpubTextFinder::htmlNodeFindRecursiveForDb(xmlNodePtr node)
422 {
423         for (xmlNodePtr cur = node; cur; cur = cur->next) {
424                 if (cur->type == XML_TEXT_NODE) {
425                         std::string text(reinterpret_cast<char*>(cur->content));
426                         batchInsert(text);
427                 }
428
429                 htmlNodeFindRecursiveForDb(cur->children);
430         }
431 }
432
433 void EpubTextFinder::htmlFindForDb(const char *html_buf, int buf_size)
434 {
435         htmlDocPtr doc = htmlReadMemory(html_buf, buf_size, "/", NULL,
436                                                                         HTML_PARSE_NOBLANKS | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING | HTML_PARSE_NONET);
437
438         if (!doc) {
439                 LOGE("htmlReadMemory failed");
440                 return;
441         }
442
443         htmlNodeFindRecursiveForDb(xmlDocGetRootElement(doc));
444         xmlFreeDoc(doc);
445 }
446
447 bool EpubTextFinder::htmlFind(const char *html_buf, int buf_size, const char *keyword)
448 {
449         htmlDocPtr doc = htmlReadMemory(html_buf, buf_size, "/", NULL,
450                                                                         HTML_PARSE_NOBLANKS | HTML_PARSE_NOERROR | HTML_PARSE_NOWARNING | HTML_PARSE_NONET);
451
452         if (!doc) {
453                 LOGE("htmlReadMemory failed");
454                 return false;
455         }
456
457         bool result = htmlNodeFindRecursive(xmlDocGetRootElement(doc), keyword);
458
459         xmlFreeDoc(doc);
460
461         return result;
462 }
463
464 extern "C" bool media_svc_pdf_is_keyword_included(const char *path, const char *keyword)
465 {
466         /* ToDo: factory pattern */
467         std::unique_ptr<TextFinderInterface> ebookText = std::make_unique<PdfTextFinder>(path);
468
469         return ebookText->find(keyword);
470 }
471
472 extern "C" bool media_svc_epub_is_keyword_included(const char *path, const char *keyword)
473 {
474         /* ToDo: factory pattern */
475         std::unique_ptr<TextFinderInterface> ebookText = std::make_unique<EpubTextFinder>(path);
476
477         return ebookText->find(keyword);
478 }
479
480 extern "C" void media_svc_pdf_insert_to_db(sqlite3 *handle, const char *path)
481 {
482         std::unique_ptr<TextFinderInterface> ebookText = std::make_unique<PdfTextFinder>(handle, path);
483
484         ebookText->insert();
485 }
486
487 extern "C" void media_svc_epub_insert_to_db(sqlite3 *handle, const char *path)
488 {
489         std::unique_ptr<TextFinderInterface> ebookText = std::make_unique<EpubTextFinder>(handle, path);
490
491         ebookText->insert();
492 }