Server: Editing notes with image insertion basically works

This commit is contained in:
Kovid Goyal 2023-10-21 12:02:45 +05:30
parent a941d66031
commit ec86d24535
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
3 changed files with 104 additions and 32 deletions

View File

@ -4,11 +4,14 @@
__license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import base64
import errno
import os
import re
from contextlib import suppress
from functools import partial
from io import BytesIO
from json import load as load_json_file
from threading import Lock
from calibre import fit_image, guess_type, sanitize_file_name
@ -22,7 +25,7 @@ from calibre.ebooks.metadata import authors_to_string
from calibre.ebooks.metadata.meta import set_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.library.save_to_disk import find_plugboard
from calibre.srv.errors import BookNotFound, HTTPNotFound
from calibre.srv.errors import BookNotFound, HTTPBadRequest, HTTPNotFound
from calibre.srv.routes import endpoint, json
from calibre.srv.utils import get_db, get_use_roman, http_date
from calibre.utils.config_base import tweaks
@ -355,7 +358,14 @@ def get(ctx, rd, what, book_id, library_id):
raise HTTPNotFound(f'No {what.lower()} format for the book {book_id!r}')
@endpoint('/get-note/{field}/{item_id}/{library_id=None}')
def resource_hash_to_url(ctx, scheme, digest, library_id):
kw = {'scheme': scheme, 'digest': digest}
if library_id:
kw['library_id'] = library_id
return ctx.url_for('/get-note-resource', **kw)
@endpoint('/get-note/{field}/{item_id}/{library_id=None}', types={'item_id': int})
def get_note(ctx, rd, field, item_id, library_id):
db = get_db(ctx, rd, library_id)
if db is None:
@ -374,10 +384,7 @@ def get_note(ctx, rd, field, item_id, library_id):
pat = re.compile(rf'{RESOURCE_URL_SCHEME}://({{}})'.format('|'.join(map(r, resources))))
def sub(m):
s, d = m.group(1).split('/', 1)
kw = {'scheme': s, 'digest': d}
if library_id:
kw['library_id'] = library_id
return ctx.url_for('/get-note-resource', **kw)
return resource_hash_to_url(ctx, s, d, library_id)
note_data['doc'] = pat.sub(sub, html)
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
rd.outheaders['Last-Modified'] = http_date(note_data['mtime'])
@ -398,3 +405,50 @@ def get_note_resource(ctx, rd, scheme, digest, library_id):
fname_for_content_disposition(name), fname_for_content_disposition(name, as_encoded_unicode=True))
rd.outheaders['Last-Modified'] = http_date(d['mtime'])
return d['data']
@endpoint('/set-note/{field}/{item_id}/{library_id=None}', needs_db_write=True, methods={'POST'}, types={'item_id': int})
def set_note(ctx, rd, field, item_id, library_id):
db = get_db(ctx, rd, library_id)
if db is None:
raise HTTPNotFound(f'Library {library_id} not found')
try:
data = load_json_file(rd.request_body_file)
if not isinstance(data, dict):
raise Exception('note data must be a dict')
html, searchable_text, images = data['html'], data['searchable_text'], data['images']
except Exception as err:
raise HTTPBadRequest(f'Invalid query: {err}')
srv_replacements = {}
db_replacements = {}
resources = []
res_pat = re.compile(r'get-note-resource/([a-zA-Z0-9]+)/([a-zA-Z0-9]+)')
for key, img in images.items():
try:
is_new_image = img['data'].startswith('data:')
if is_new_image:
d = img['data'].encode('ascii')
idx = d.index(b',')
d = memoryview(d)[idx:]
img_data = base64.standard_b64decode(d)
fname = img['filename']
else:
m = res_pat.search(img['data'])
scheme, digest = m.group(1), m.group(2)
resources.append(f'{scheme}:{digest}')
except Exception as err:
raise HTTPBadRequest(f'Invalid query: {err}')
if is_new_image:
chash = db.add_notes_resource(img_data, fname)
scheme, digest = chash.split(':', 1)
resources.append(chash)
srv_replacements[key] = resource_hash_to_url(ctx, scheme, digest, library_id)
db_replacements[key] = f'{RESOURCE_URL_SCHEME}://{scheme}/{digest}'
db_html = srv_html = html
if db_replacements:
db_html = re.sub('|'.join(map(re.escape, db_replacements)), lambda m: db_replacements[m.group()], html)
if srv_replacements:
srv_html = re.sub('|'.join(map(re.escape, srv_replacements)), lambda m: srv_replacements[m.group()], html)
db.set_notes_for(field, item_id, db_html, searchable_text, resources, True)
rd.outheaders['Content-Type'] = 'text/html; charset=UTF-8'
return srv_html

View File

@ -419,21 +419,16 @@ class CommentsEditorBoss:
document.body.lastChild.innerHTML = data.html
def get_html(self, data):
c = document.body.lastChild
c = document.body.lastChild.cloneNode(True)
images = v'{}'
for img in c.getElementByTagName('img'):
if img.src and img.src.startsWith('data:'):
if data.extract_images:
for img in c.getElementsByTagName('img'):
if img.src:
key = short_uuid4()
images[key] = {'data': img.src, 'filename': img.dataset.filename}
img.src = key
v'delete img.dataset.filename'
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images})
for img in c.getElementByTagName('img'):
if img.src:
d = images[img.src]
if d:
img.src = d.data
img.dataset.filename = d.filename
self.comm.send_message('html', html=c.innerHTML, extra_data={'images': images, 'text': c.innerText})
def exec_command(self, data):
document.execCommand(data.name, False, data.value)
@ -469,6 +464,7 @@ class Editor:
self.id = iframe.id
self.ready = False
self.pending_set_html = None
self.pending_get_html = v'[]'
self.get_html_callbacks = v'[]'
self.iframe_obj = iframe
@ -496,8 +492,9 @@ class Editor:
if self.pending_set_html is not None:
self.set_html(self.pending_set_html)
self.pending_set_html = None
if self.get_html_callbacks.length:
self.get_html(self.get_html_callback)
if self.pending_get_html.length > 0:
for x in self.pending_get_html:
self.get_html(x.proceed, x.extract_images)
def set_html(self, html):
if not self.ready:
@ -506,15 +503,17 @@ class Editor:
rgba = get_color_as_rgba('window-foreground')
self.iframe_wrapper.send_message('set_html', html=html, color_scheme=color_scheme(), color=f'rgba({rgba[0]},{rgba[1]},{rgba[2]},{rgba[3]})')
def get_html(self, proceed):
def get_html(self, proceed, extract_images):
self.get_html_callbacks.push(proceed)
if self.ready:
self.iframe_wrapper.send_message('get_html')
self.iframe_wrapper.send_message('get_html', extract_images=v'!!extract_images')
else:
self.pending_get_html.push({'proceed': proceed, 'extract_images': v'!!extract_images'})
def on_html_received(self, data):
if self.get_html_callbacks.length:
for f in self.get_html_callbacks:
f(data.html, data.img_urls)
f(data.html, data.extra_data)
self.get_html_callbacks = v'[]'
def exec_command(self, name, value=None):
@ -607,11 +606,11 @@ def set_comments_html(container, html):
editor.set_html(html or '')
def get_comments_html(container, proceed):
def get_comments_html(container, proceed, extract_images):
iframe = container.querySelector('iframe')
eid = iframe.getAttribute('id')
editor = registry[eid]
editor.get_html(proceed)
editor.get_html(proceed, extract_images)
def develop(container):

View File

@ -13,6 +13,7 @@ from book_list.router import back, home, show_note
from book_list.top_bar import add_button, create_top_bar
from book_list.ui import set_panel_handler
from gettext import gettext as _
from modals import ajax_send_progress_dialog, error_dialog
from utils import parse_url_params, safe_set_inner_html, sandboxed_html
@ -144,11 +145,29 @@ def save(container_id):
c = document.getElementById(container_id)
if not c:
return
get_comments_html(c, def(html, extra_data):
print(html)
console.log(extra_data)
print('TODO: Implement saving')
)
q = parse_url_params()
url = 'set-note/' + encodeURIComponent(q.field) + '/' + encodeURIComponent(q.item_id)
if q.library_id:
url += '/' + encodeURIComponent(q.library_id)
close_action = get_close_action()[0]
def on_complete(end_type, xhr, evt):
nonlocal current_note_markup
if end_type is 'abort':
close_action()
return
if end_type is not 'load':
error_dialog(_('Failed to send notes to server'), _(
'Updating the notes for: {} failed.').format(q.item), xhr.error_html)
return
q.html = xhr.responseText
current_note_markup = q
close_action()
def on_got_html(html, extra_data):
ajax_send_progress_dialog(url, {'html': html, 'searchable_text': extra_data.text, 'images': extra_data.images}, on_complete, _('Sending note data to calibre server...'))
get_comments_html(c, on_got_html, True)
def init_edit(container_id):