Realign to trunk.

This commit is contained in:
John Schember 2009-12-19 15:58:51 -05:00
parent 9ee3e926c0
commit 86afb92057
13 changed files with 152 additions and 91 deletions

View File

@ -13,4 +13,11 @@ base_dir = os.path.join(src_dir, 'calibre')
vipy.session.initialize(project_name='calibre', src_dir=src_dir, vipy.session.initialize(project_name='calibre', src_dir=src_dir,
project_dir=project_dir, base_dir=base_dir) project_dir=project_dir, base_dir=base_dir)
def recipe_title_callback(raw):
return eval(raw.decode('utf-8'))
vipy.session.add_content_browser('.r', ',r', 'Recipe',
vipy.session.glob_based_iterator(os.path.join(project_dir, 'resources', 'recipes', '*.recipe')),
vipy.session.regexp_based_matcher(r'title\s*=\s*(?P<title>.+)', 'title', recipe_title_callback))
EOFPY EOFPY

View File

@ -47,25 +47,24 @@ class CYBOOKG3(USBMS):
DELETE_EXTS = ['.mbp', '.dat', '_6090.t2b'] DELETE_EXTS = ['.mbp', '.dat', '_6090.t2b']
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
def upload_books(self, files, metadatas, ids, on_card=None, def upload_books(self, files, names, on_card=None, end_session=True,
end_session=True): metadata=None):
path = self._sanity_check(on_card, files) path = self._sanity_check(on_card, files)
paths = [] paths = []
metadatas = iter(metadatas) names = iter(names)
ids = iter(ids) metadata = iter(metadata)
for i, infile in enumerate(files): for i, infile in enumerate(files):
mdata, id = metadatas.next(), ids.next() mdata, fname = metadata.next(), names.next()
ext = os.path.splitext(infile)[1] filepath = self.create_upload_path(path, mdata, fname)
filepath = self.create_upload_path(path, mdata, ext, id)
paths.append(filepath) paths.append(filepath)
self.put_file(infile, filepath, replace_file=True) self.put_file(infile, filepath, replace_file=True)
coverdata = None coverdata = None
cover = mdata.cover cover = mdata.get('cover', None)
if cover: if cover:
coverdata = cover[2] coverdata = cover[2]

View File

@ -35,7 +35,7 @@ class IRIVER_STORY(USBMS):
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
def windows_sort_drives(self, drives): def windows_open_callback(self, drives):
main = drives.get('main', None) main = drives.get('main', None)
card = drives.get('carda', None) card = drives.get('carda', None)
if card and main and card < main: if card and main and card < main:

View File

@ -15,7 +15,7 @@ from itertools import cycle
from calibre.devices.usbms.driver import USBMS from calibre.devices.usbms.driver import USBMS
from calibre.utils.filenames import ascii_filename as sanitize from calibre.utils.filenames import ascii_filename as sanitize
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import string_to_authors
class JETBOOK(USBMS): class JETBOOK(USBMS):
name = 'Ectaco JetBook Device Interface' name = 'Ectaco JetBook Device Interface'
@ -50,22 +50,23 @@ class JETBOOK(USBMS):
r'(?P<authors>.+)#(?P<title>.+)' r'(?P<authors>.+)#(?P<title>.+)'
) )
def upload_books(self, files, metadatas, ids, on_card=None, def upload_books(self, files, names, on_card=False, end_session=True,
end_session=True): metadata=None):
path = self._sanity_check(on_card, files)
base_path = self._sanity_check(on_card, files)
paths = [] paths = []
metadatas = iter(metadatas) names = iter(names)
ids = iter(ids) metadata = iter(metadata)
for i, infile in enumerate(files): for i, infile in enumerate(files):
mdata, id = metadatas.next(), ids.next() mdata, fname = metadata.next(), names.next()
ext = os.path.splitext(infile)[1] path = os.path.dirname(self.create_upload_path(base_path, mdata, fname))
path = self.create_upload_path(path, mdata, ext, id)
author = sanitize(authors_to_string(mdata.authors)).replace(' ', '_') author = sanitize(mdata.get('authors','Unknown')).replace(' ', '_')
title = sanitize(mdata.title).replace(' ', '_') title = sanitize(mdata.get('title', 'Unknown')).replace(' ', '_')
fname = '%s#%s%s' % (author, title, ext) fileext = os.path.splitext(os.path.basename(fname))[1]
fname = '%s#%s%s' % (author, title, fileext)
filepath = os.path.join(path, fname) filepath = os.path.join(path, fname)
paths.append(filepath) paths.append(filepath)

View File

@ -9,7 +9,6 @@ from base64 import b64decode as decode
from base64 import b64encode as encode from base64 import b64encode as encode
import re import re
from calibre.ebooks.metadata import authors_to_string
from calibre.devices.interface import BookList as _BookList from calibre.devices.interface import BookList as _BookList
from calibre.devices import strftime, strptime from calibre.devices import strftime, strptime
@ -263,9 +262,9 @@ class BookList(_BookList):
cid = self.max_id()+1 cid = self.max_id()+1
sourceid = str(self[0].sourceid) if len(self) else "1" sourceid = str(self[0].sourceid) if len(self) else "1"
attrs = { attrs = {
"title" : info.title, "title" : info["title"],
'titleSorter' : sortable_title(info.title), 'titleSorter' : sortable_title(info['title']),
"author" : authors_to_string(info.authors), \ "author" : info["authors"] if info['authors'] else 'Unknown', \
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size) "mime":mime, "path":name, "size":str(size)
@ -274,7 +273,7 @@ class BookList(_BookList):
node.setAttributeNode(self.document.createAttribute(attr)) node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr]) node.setAttribute(attr, attrs[attr])
try: try:
w, h, data = info.cover w, h, data = info["cover"]
except TypeError: except TypeError:
w, h, data = None, None, None w, h, data = None, None, None
@ -291,7 +290,10 @@ class BookList(_BookList):
book.datetime = ctime book.datetime = ctime
self.append(book) self.append(book)
self.set_next_id(cid+1) self.set_next_id(cid+1)
self.set_playlists(book.id, info.tags) if self.prefix and info.has_key('tags'): # Playlists only supportted in main memory
if info.has_key('tag order'):
self.tag_order.update(info['tag order'])
self.set_playlists(book.id, info['tags'])
def playlist_by_title(self, title): def playlist_by_title(self, title):

View File

@ -867,14 +867,14 @@ class PRS500(DeviceConfig, DevicePlugin):
self.upload_book_list(booklists[1], end_session=False) self.upload_book_list(booklists[1], end_session=False)
@safe @safe
def upload_books(self, files, metadatas, ids, on_card=None, def upload_books(self, files, names, on_card=False, end_session=True,
end_session=True): metadata=None):
card = self.card(end_session=False) card = self.card(end_session=False)
prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/' prefix = card + '/' + self.CARD_PATH_PREFIX +'/' if on_card else '/Data/media/books/'
if on_card and not self._exists(prefix)[0]: if on_card and not self._exists(prefix)[0]:
self.mkdir(prefix[:-1], False) self.mkdir(prefix[:-1], False)
paths, ctimes = [], [] paths, ctimes = [], []
names = iter([m.title for m in metatdatas]) names = iter(names)
infiles = [file if hasattr(file, 'read') else open(file, 'rb') for file in files] infiles = [file if hasattr(file, 'read') else open(file, 'rb') for file in files]
for f in infiles: f.seek(0, 2) for f in infiles: f.seek(0, 2)
sizes = [f.tell() for f in infiles] sizes = [f.tell() for f in infiles]

View File

@ -8,7 +8,7 @@ import xml.dom.minidom as dom
from base64 import b64decode as decode from base64 import b64decode as decode
from base64 import b64encode as encode from base64 import b64encode as encode
from calibre.ebooks.metadata import authors_to_string
from calibre.devices.interface import BookList as _BookList from calibre.devices.interface import BookList as _BookList
from calibre.devices import strftime as _strftime from calibre.devices import strftime as _strftime
from calibre.devices import strptime from calibre.devices import strptime
@ -194,9 +194,9 @@ class BookList(_BookList):
except: except:
sourceid = '1' sourceid = '1'
attrs = { attrs = {
"title" : info.title, "title" : info["title"],
'titleSorter' : sortable_title(info.title), 'titleSorter' : sortable_title(info['title']),
"author" : authors_to_string(info.authors), "author" : info["authors"] if info['authors'] else _('Unknown'),
"page":"0", "part":"0", "scale":"0", \ "page":"0", "part":"0", "scale":"0", \
"sourceid":sourceid, "id":str(cid), "date":"", \ "sourceid":sourceid, "id":str(cid), "date":"", \
"mime":mime, "path":name, "size":str(size) "mime":mime, "path":name, "size":str(size)
@ -205,7 +205,7 @@ class BookList(_BookList):
node.setAttributeNode(self.document.createAttribute(attr)) node.setAttributeNode(self.document.createAttribute(attr))
node.setAttribute(attr, attrs[attr]) node.setAttribute(attr, attrs[attr])
try: try:
w, h, data = info.cover w, h, data = info["cover"]
except TypeError: except TypeError:
w, h, data = None, None, None w, h, data = None, None, None
@ -221,7 +221,10 @@ class BookList(_BookList):
book = Book(node, self.mountpath, [], prefix=self.prefix) book = Book(node, self.mountpath, [], prefix=self.prefix)
book.datetime = ctime book.datetime = ctime
self.append(book) self.append(book)
self.set_tags(book, info.tags) if info.has_key('tags'):
if info.has_key('tag order'):
self.tag_order.update(info['tag order'])
self.set_tags(book, info['tags'])
def _delete_book(self, node): def _delete_book(self, node):
nid = node.getAttribute('id') nid = node.getAttribute('id')

View File

@ -114,22 +114,20 @@ class PRS505(CLI, Device):
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return bl return bl
def upload_books(self, files, metadatas, ids, on_card=None, def upload_books(self, files, names, on_card=None, end_session=True,
end_session=True): metadata=None):
path = self._sanity_check(on_card, files) path = self._sanity_check(on_card, files)
paths = [] paths, ctimes, sizes = [], [], []
metadatas = iter(metadatas) names = iter(names)
ids = iter(ids) metadata = iter(metadata)
for i, infile in enumerate(files): for i, infile in enumerate(files):
mdata, id = metadatas.next(), ids.next() mdata, fname = metadata.next(), names.next()
ext = os.path.splitext(infile)[1] filepath = self.create_upload_path(path, mdata, fname)
filepath = self.create_upload_path(path, mdata, ext, id)
paths.append(filepath)
self.put_file(infile, filepath, replace_file=True) paths.append(filepath)
self.put_file(infile, paths[-1], replace_file=True)
ctimes.append(os.path.getctime(paths[-1])) ctimes.append(os.path.getctime(paths[-1]))
sizes.append(os.stat(paths[-1]).st_size) sizes.append(os.stat(paths[-1]).st_size)

View File

@ -23,7 +23,7 @@ from calibre.devices.interface import DevicePlugin
from calibre.devices.errors import DeviceError, FreeSpaceError from calibre.devices.errors import DeviceError, FreeSpaceError
from calibre.devices.usbms.deviceconfig import DeviceConfig from calibre.devices.usbms.deviceconfig import DeviceConfig
from calibre import iswindows, islinux, isosx, __appname__ from calibre import iswindows, islinux, isosx, __appname__
from calibre.utils.filenames import shorten_components_to from calibre.utils.filenames import ascii_filename as sanitize, shorten_components_to
class Device(DeviceConfig, DevicePlugin): class Device(DeviceConfig, DevicePlugin):
@ -295,20 +295,20 @@ class Device(DeviceConfig, DevicePlugin):
# This is typically needed when the device has the same # This is typically needed when the device has the same
# WINDOWS_MAIN_MEM and WINDOWS_CARD_A_MEM in which case # WINDOWS_MAIN_MEM and WINDOWS_CARD_A_MEM in which case
# if the devices is connected without a crad, the above # if the devices is connected without a card, the above
# will incorrectly identify the main mem as carda # will incorrectly identify the main mem as carda
# See for example the driver for the Nook # See for example the driver for the Nook
if 'main' not in drives and 'carda' in drives: if 'main' not in drives and 'carda' in drives:
drives['main'] = drives.pop('carda') drives['main'] = drives.pop('carda')
drives = self.windows_open_callback(drives) drives = self.windows_open_callback(drives)
drives = self.windows_sort_drives(drives)
if drives.get('main', None) is None: if drives.get('main', None) is None:
raise DeviceError( raise DeviceError(
_('Unable to detect the %s disk drive. Try rebooting.') % _('Unable to detect the %s disk drive. Try rebooting.') %
self.__class__.__name__) self.__class__.__name__)
drives = self.windows_sort_drives(drives)
self._main_prefix = drives.get('main') self._main_prefix = drives.get('main')
self._card_a_prefix = drives.get('carda', None) self._card_a_prefix = drives.get('carda', None)
self._card_b_prefix = drives.get('cardb', None) self._card_b_prefix = drives.get('cardb', None)
@ -739,18 +739,54 @@ class Device(DeviceConfig, DevicePlugin):
raise FreeSpaceError(_("There is insufficient free space on the storage card")) raise FreeSpaceError(_("There is insufficient free space on the storage card"))
return path return path
def create_upload_path(self, root, mdata, ext, id): def create_upload_path(self, path, mdata, fname):
from calibre.library.save_to_disk import config, get_components path = os.path.abspath(path)
opts = config().parse() newpath = path
components = get_components(opts.template, mdata, id, opts.timefmt, 250) extra_components = []
components = [str(x) for x in components]
components = shorten_components_to(250 - len(root), components) if self.SUPPORTS_SUB_DIRS and self.settings().use_subdirs:
filepath = '%s%s' % (os.path.join(root, *components), ext) if 'tags' in mdata.keys():
for tag in mdata['tags']:
if tag.startswith(_('News')):
extra_components.append('news')
c = sanitize(mdata.get('title', ''))
if c:
extra_components.append(c)
c = sanitize(mdata.get('timestamp', ''))
if c:
extra_components.append(c)
break
elif tag.startswith('/'):
for c in tag.split('/'):
c = sanitize(c)
if not c: continue
extra_components.append(c)
break
if not extra_components:
c = sanitize(mdata.get('authors', _('Unknown')))
if c:
extra_components.append(c)
c = sanitize(mdata.get('title', _('Unknown')))
if c:
extra_components.append(c)
newpath = os.path.join(newpath, c)
fname = sanitize(fname)
extra_components.append(fname)
extra_components = [str(x) for x in extra_components]
def remove_trailing_periods(x):
ans = x
while ans.endswith('.'):
ans = ans[:-1]
if not ans:
ans = 'x'
return ans
extra_components = list(map(remove_trailing_periods, extra_components))
components = shorten_components_to(250 - len(path), extra_components)
filepath = os.path.join(path, *components)
filedir = os.path.dirname(filepath) filedir = os.path.dirname(filepath)
if not self.SUPPORTS_SUB_DIRS or not self.settings().use_subdirs:
filedir = root
filepath = os.path.join(root, os.path.basename(filepath))
if not os.path.exists(filedir): if not os.path.exists(filedir):
os.makedirs(filedir) os.makedirs(filedir)

View File

@ -95,19 +95,19 @@ class USBMS(CLI, Device):
return bl return bl
def upload_books(self, files, metadatas, ids, on_card=None, def upload_books(self, files, names, on_card=None, end_session=True,
end_session=True): metadata=None):
path = self._sanity_check(on_card, files) path = self._sanity_check(on_card, files)
paths = [] paths = []
metadatas = iter(metadatas) names = iter(names)
ids = iter(ids) metadata = iter(metadata)
for i, infile in enumerate(files): for i, infile in enumerate(files):
mdata, id = metadatas.next(), ids.next() mdata, fname = metadata.next(), names.next()
ext = os.path.splitext(infile)[1] filepath = self.create_upload_path(path, mdata, fname)
filepath = self.create_upload_path(path, mdata, ext, id)
paths.append(filepath) paths.append(filepath)
self.put_file(infile, filepath, replace_file=True) self.put_file(infile, filepath, replace_file=True)

View File

@ -223,17 +223,18 @@ class DeviceManager(Thread):
return self.create_job(self._sync_booklists, done, args=[booklists], return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device')) description=_('Send metadata to device'))
def _upload_books(self, files, metadata, ids, on_card=None): def _upload_books(self, files, names, on_card=None, metadata=None):
'''Upload books to device: ''' '''Upload books to device: '''
return self.device.upload_books(files, metadata, ids, on_card, return self.device.upload_books(files, names, on_card,
end_session=False) metadata=metadata, end_session=False)
def upload_books(self, done, files, metadata, ids, on_card=None, titles=None): def upload_books(self, done, files, names, on_card=None, titles=None,
desc = _('Upload %d books to device')%len(files) metadata=None):
desc = _('Upload %d books to device')%len(names)
if titles: if titles:
desc += u':' + u', '.join(titles) desc += u':' + u', '.join(titles)
return self.create_job(self._upload_books, done, args=[files, metadata, ids], return self.create_job(self._upload_books, done, args=[files, names],
kwargs={'on_card':on_card}, description=desc) kwargs={'on_card':on_card,'metadata':metadata}, description=desc)
def add_books_to_metadata(self, locations, metadata, booklists): def add_books_to_metadata(self, locations, metadata, booklists):
self.device.add_books_to_metadata(locations, metadata, booklists) self.device.add_books_to_metadata(locations, metadata, booklists)
@ -707,18 +708,18 @@ class DeviceGUI(object):
dynamic.set('news_to_be_synced', set([])) dynamic.set('news_to_be_synced', set([]))
return return
metadata = self.library_view.model().get_metadata(ids, metadata = self.library_view.model().get_metadata(ids,
rows_are_ids=True, full_metadata=True)[1] rows_are_ids=True)
names = [] names = []
for mi in metadata: for mi in metadata:
prefix = ascii_filename(mi.title) prefix = ascii_filename(mi['title'])
if not isinstance(prefix, unicode): if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace') prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix) prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id, names.append('%s_%d%s'%(prefix, id,
os.path.splitext(f.name)[1])) os.path.splitext(f.name)[1]))
cdata = mi.cover cdata = mi['cover']
if cdata: if cdata:
mi.cover = self.cover_to_thumbnail(cdata) mi['cover'] = self.cover_to_thumbnail(cdata)
dynamic.set('news_to_be_synced', set([])) dynamic.set('news_to_be_synced', set([]))
if config['upload_news_to_device'] and files: if config['upload_news_to_device'] and files:
remove = ids if \ remove = ids if \
@ -727,7 +728,8 @@ class DeviceGUI(object):
self.location_view.model().free[1] : 'carda', self.location_view.model().free[1] : 'carda',
self.location_view.model().free[2] : 'cardb' } self.location_view.model().free[2] : 'cardb' }
on_card = space.get(sorted(space.keys(), reverse=True)[0], None) on_card = space.get(sorted(space.keys(), reverse=True)[0], None)
self.upload_books(files, metadata, ids, on_card=on_card, self.upload_books(files, names, metadata,
on_card=on_card,
memory=[[f.name for f in files], remove]) memory=[[f.name for f in files], remove])
self.status_bar.showMessage(_('Sending news to device.'), 5000) self.status_bar.showMessage(_('Sending news to device.'), 5000)
@ -749,28 +751,38 @@ class DeviceGUI(object):
else: else:
_auto_ids = [] _auto_ids = []
metadata = self.library_view.model().get_metadata(ids, True, full_metadata=True)[1] metadata = self.library_view.model().get_metadata(ids, True)
ids = iter(ids) ids = iter(ids)
for mi in metadata: for mi in metadata:
cdata = mi.cover cdata = mi['cover']
if cdata: if cdata:
mi['cover'] = self.cover_to_thumbnail(cdata) mi['cover'] = self.cover_to_thumbnail(cdata)
metadata = iter(metadata) metadata = iter(metadata)
files = [getattr(f, 'name', None) for f in _files] files = [getattr(f, 'name', None) for f in _files]
bad, mdata, gf, fids, remove_ids = [], [], [], [], [] bad, good, gf, names, remove_ids = [], [], [], [], []
for f in files: for f in files:
mi = metadata.next() mi = metadata.next()
id = ids.next() id = ids.next()
if f is None: if f is None:
bad.append(mi.title) bad.append(mi['title'])
else: else:
remove_ids.append(id) remove_ids.append(id)
good.append(mi)
gf.append(f) gf.append(f)
mdata.append(mi) t = mi['title']
fids.append(id) if not t:
t = _('Unknown')
a = mi['authors']
if not a:
a = _('Unknown')
prefix = ascii_filename(t+' - '+a)
if not isinstance(prefix, unicode):
prefix = prefix.decode(preferred_encoding, 'replace')
prefix = ascii_filename(prefix)
names.append('%s_%d%s'%(prefix, id, os.path.splitext(f)[1]))
remove = remove_ids if delete_from_library else [] remove = remove_ids if delete_from_library else []
self.upload_books(gf, mdata, fids, on_card, memory=(_files, remove)) self.upload_books(gf, names, good, on_card, memory=(_files, remove))
self.status_bar.showMessage(_('Sending books to device.'), 5000) self.status_bar.showMessage(_('Sending books to device.'), 5000)
auto = [] auto = []
@ -833,15 +845,17 @@ class DeviceGUI(object):
cp, fs = job.result cp, fs = job.result
self.location_view.model().update_devices(cp, fs) self.location_view.model().update_devices(cp, fs)
def upload_books(self, files, metadata, ids, on_card=None, memory=None): def upload_books(self, files, names, metadata, on_card=None, memory=None):
''' '''
Upload books to device. Upload books to device.
:param files: List of either paths to files or file like objects :param files: List of either paths to files or file like objects
''' '''
titles = [i.title for i in metadata] titles = [i['title'] for i in metadata]
job = self.device_manager.upload_books( job = self.device_manager.upload_books(
Dispatcher(self.books_uploaded), Dispatcher(self.books_uploaded),
files, metadata, ids, on_card=on_card, titles=titles) files, names, on_card=on_card,
metadata=metadata, titles=titles
)
self.upload_memory[job] = (metadata, on_card, memory, files) self.upload_memory[job] = (metadata, on_card, memory, files)
def books_uploaded(self, job): def books_uploaded(self, job):
@ -854,7 +868,7 @@ class DeviceGUI(object):
if isinstance(job.exception, FreeSpaceError): if isinstance(job.exception, FreeSpaceError):
where = 'in main memory.' if 'memory' in str(job.exception) \ where = 'in main memory.' if 'memory' in str(job.exception) \
else 'on the storage card.' else 'on the storage card.'
titles = '\n'.join(['<li>'+mi.title+'</li>' \ titles = '\n'.join(['<li>'+mi['title']+'</li>' \
for mi in metadata]) for mi in metadata])
d = error_dialog(self, _('No space on device'), d = error_dialog(self, _('No space on device'),
_('<p>Cannot upload books to device there ' _('<p>Cannot upload books to device there '

View File

@ -70,7 +70,7 @@
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Here you can control how calibre will save your books when you click the Save to Disk or Send to Device buttons:</string> <string>Here you can control how calibre will save your books when you click the Save to Disk button:</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>

View File

@ -497,6 +497,7 @@ TXT input supports a number of options to differentiate how paragraphs are detec
Convert PDF documents Convert PDF documents
~~~~~~~~~~~~~~~~~~~~~~~~~~~
PDF documents are one of the worst formats to convert from. They are a fixed page size and text placement format. PDF documents are one of the worst formats to convert from. They are a fixed page size and text placement format.
Meaning, it is very difficult to determine where one paragraph ends and another begins. |app| will try to unwrap Meaning, it is very difficult to determine where one paragraph ends and another begins. |app| will try to unwrap