Project

General

Profile

bug #7699

NPE when searching for specimen in bulk editor

Added by Katja Luther almost 2 years ago. Updated 2 months ago.

Status:
New
Priority:
Highest
Assignee:
Category:
taxeditor
Target version:
Start date:
08/29/2018
Due date:
% Done:

0%

Severity:
normal
Found in Version:

Description

NPE when searching for specimen

NOTE: in #8626 detailed instructions are given which may help reproducing this issue

login : 
editor version : 5.3.0.201808280657
server :  ()
schema version : 
os : Windows Server 2012 R2 6.3 amd64
java : 1.8.0_121
org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at [http://test.e-taxonomy.eu:80/cdmserver/rem_conf_am/remoting/occurrence.service]; nested exception is java.lang.NullPointerException
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:216)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:147)
                at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
                at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
                at com.sun.proxy.$Proxy96.findByTitle(Unknown Source)
                at eu.etaxonomy.taxeditor.store.SearchManager.findOccurrences(SearchManager.java:158)
                at eu.etaxonomy.taxeditor.store.SearchManager.findOccurrences(SearchManager.java:115)
                at eu.etaxonomy.taxeditor.bulkeditor.input.OccurrenceEditorInput.listEntities(OccurrenceEditorInput.java:70)
                at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput.lambda$0(AbstractBulkEditorInput.java:219)
                at org.eclipse.core.runtime.jobs.Job$2.run(Job.java:186)
                at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.NullPointerException
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getCacheElement(CdmTransientEntityCacher.java:254)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getFromCache(CdmTransientEntityCacher.java:259)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.put(CdmTransientEntityCacher.java:235)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:273)
                at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:298)
                at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:87)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:189)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:160)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:72)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.load(CdmTransientEntityCacher.java:132)
                at eu.etaxonomy.taxeditor.session.CdmEntitySession.load(CdmEntitySession.java:77)
                at eu.etaxonomy.taxeditor.session.CdmEntitySessionManager.load(CdmEntitySessionManager.java:131)
                at eu.etaxonomy.taxeditor.service.CdmServiceRequestExecutor.doExecuteRequest(CdmServiceRequestExecutor.java:75)
                at org.springframework.remoting.httpinvoker.AbstractHttpInvokerRequestExecutor.executeRequest(AbstractHttpInvokerRequestExecutor.java:138)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:194)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:176)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:144)
                ... 9 more


Related issues

Duplicated by Edit - bug #8626: NullPointerException (NPE) on saving Specimen Editor (list) Duplicate 10/29/2019
Duplicated by Edit - bug #8923: NullPointerException (NPE) after attempt to save invalid Group name Duplicate 04/01/2020

Associated revisions

Revision a44c8394 (diff)
Added by Andreas Müller over 1 year ago

ref #7699 Allow 3 representations for CdmImportBase.getImageMedia

History

#2 Updated by Patrick Plitzner almost 2 years ago

  • Status changed from New to Feedback
  • Assignee changed from Patrick Plitzner to Andreas Müller

I cannot reproduce this. On which DB did you test? Does it always happen for any search on any DB?

#3 Updated by Andreas Müller almost 2 years ago

On rem_conf_am (see stacktrace) and search for "A*"

#4 Updated by Andreas Müller almost 2 years ago

  • Assignee changed from Andreas Müller to Patrick Plitzner

#5 Updated by Patrick Plitzner almost 2 years ago

  • Assignee changed from Patrick Plitzner to Katja Luther

Andreas Müller wrote:

On rem_conf_am (see stacktrace) and search for "A*"

I cannot reproduce this here either

#6 Updated by Andreas Müller almost 2 years ago

It still exists sometimes

login : 
editor version : 5.3.0.201809042248
server :  ()
schema version : 
os : Windows Server 2012 R2 6.3 amd64
java : 1.8.0_121
org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at [http://test.e-taxonomy.eu:80/cdmserver/rem_conf_am/remoting/reference.service]; nested exception is java.lang.NullPointerException
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:216)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:147)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy58.findByTitle(Unknown Source)
    at eu.etaxonomy.taxeditor.store.SearchManager.findReferences(SearchManager.java:89)
    at eu.etaxonomy.taxeditor.bulkeditor.input.ReferenceEditorInput.listEntities(ReferenceEditorInput.java:102)
    at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput.lambda$0(AbstractBulkEditorInput.java:219)
    at org.eclipse.core.runtime.jobs.Job$2.run(Job.java:186)
    at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.NullPointerException
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getCacheElement(CdmTransientEntityCacher.java:254)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getFromCache(CdmTransientEntityCacher.java:259)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.put(CdmTransientEntityCacher.java:235)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:268)
    at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:293)
    at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:84)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:184)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:155)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:69)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.load(CdmTransientEntityCacher.java:131)
    at eu.etaxonomy.taxeditor.session.CdmEntitySession.load(CdmEntitySession.java:70)
    at eu.etaxonomy.taxeditor.session.CdmEntitySessionManager.load(CdmEntitySessionManager.java:131)
    at eu.etaxonomy.taxeditor.service.CdmServiceRequestExecutor.doExecuteRequest(CdmServiceRequestExecutor.java:75)
    at org.springframework.remoting.httpinvoker.AbstractHttpInvokerRequestExecutor.executeRequest(AbstractHttpInvokerRequestExecutor.java:138)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:194)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:176)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:144)
    ... 8 more

my idea was that it happens if you start search while the views still refresh and tried to reproduce

login : 
editor version : 5.3.0.201809042248
server :  ()
schema version : 
os : Windows Server 2012 R2 6.3 amd64
java : 1.8.0_121
org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at [http://test.e-taxonomy.eu:80/cdmserver/rem_conf_am/remoting/reference.service]; nested exception is java.lang.NullPointerException
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:216)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:147)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy58.findByTitle(Unknown Source)
    at eu.etaxonomy.taxeditor.store.SearchManager.findReferences(SearchManager.java:89)
    at eu.etaxonomy.taxeditor.bulkeditor.input.ReferenceEditorInput.listEntities(ReferenceEditorInput.java:102)
    at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput.lambda$0(AbstractBulkEditorInput.java:219)
    at org.eclipse.core.runtime.jobs.Job$2.run(Job.java:186)
    at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.NullPointerException

This was reproducable after it happended once, even in other bulk editors. Only restarting the TaxEditor solved the issue. After restart also the first error could not be reproduced. So it seems to happen only in a certain state of the TaxEditor

However, even if difficult to reproduce we should think about handling the NPE case. What if getCache() returns null. Should we create a new cache and merge all existing data into it?

Also we should have a look why the cache can be null. Something wrong with dispose handling in special cases. Maybe a "disposed" flag can help to find the reason. Once the cacher is disposed the flag should be set to true. Calling getCache() on disposed Cacher then throws a specific Exception saying that this is not allowed.

Or do we reuse Cachers after dispose was called?

#7 Updated by Andreas Müller almost 2 years ago

Added AK as this is maybe a general ticket about handling of disposed in CdmTransientEntityCacher so might be interesting for him too.

#8 Updated by Katja Luther almost 2 years ago

  • Target version changed from Release 5.3 to Release 5.4

#9 Updated by Andreas Kohlbecker over 1 year ago

I also had it today in the Users Bulkeditor:

login : 
editor version : 5.4.0.201810151448
server :  ()
schema version : 
os : Linux 4.15.0-34-generic amd64
java : 1.8.0_131
org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at [http://160.45.63.231:80/cdmserver/phycobank_01/remoting-public/user.service]; nested exception is java.lang.NullPointerException
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:216)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:147)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
    at com.sun.proxy.$Proxy25.listByUsername(Unknown Source)
    at eu.etaxonomy.taxeditor.store.SearchManager.findUsers(SearchManager.java:166)
    at eu.etaxonomy.taxeditor.bulkeditor.input.UserEditorInput.listEntities(UserEditorInput.java:79)
    at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput.lambda$0(AbstractBulkEditorInput.java:273)
    at org.eclipse.core.runtime.jobs.Job$2.run(Job.java:186)
    at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.NullPointerException
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getCacheElement(CdmTransientEntityCacher.java:288)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getFromCache(CdmTransientEntityCacher.java:293)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.put(CdmTransientEntityCacher.java:261)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:268)
    at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:293)
    at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:84)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:184)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:155)
    at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:67)
    at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.load(CdmTransientEntityCacher.java:157)
    at eu.etaxonomy.taxeditor.session.CdmEntitySession.load(CdmEntitySession.java:70)
    at eu.etaxonomy.taxeditor.session.CdmEntitySessionManager.load(CdmEntitySessionManager.java:131)
    at eu.etaxonomy.taxeditor.service.CdmServiceRequestExecutor.doExecuteRequest(CdmServiceRequestExecutor.java:75)
    at org.springframework.remoting.httpinvoker.AbstractHttpInvokerRequestExecutor.executeRequest(AbstractHttpInvokerRequestExecutor.java:138)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:194)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:176)
    at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:144)
    ... 8 more

#10 Updated by Andreas Müller over 1 year ago

  • Target version changed from Release 5.4 to Release 5.5

As this does not seem to be solved yet I move it to the next version.

#11 Updated by Katja Luther over 1 year ago

the cache is disposed if the cdmEntitySession is closed, for the bulkeditor this is the case when the editor is saved or closed. But I could not reproduce this exception whether for closing nor for saving and searching in parallel

#12 Updated by Katja Luther over 1 year ago

  • Target version changed from Release 5.5 to Release 5.6

#13 Updated by Andreas Müller over 1 year ago

  • Target version changed from Release 5.6 to Reviewed Next Major Release

#14 Updated by Andreas Müller 8 months ago

  • Related to bug #8626: NullPointerException (NPE) on saving Specimen Editor (list) added

#15 Updated by Katja Luther 8 months ago

  • Related to deleted (bug #8626: NullPointerException (NPE) on saving Specimen Editor (list))

#16 Updated by Katja Luther 8 months ago

  • Duplicated by bug #8626: NullPointerException (NPE) on saving Specimen Editor (list) added

#17 Updated by Andreas Müller 6 months ago

  • Status changed from Feedback to New
  • Target version changed from Reviewed Next Major Release to Release 5.13

This exception appears quite often therefore I put it to the current milestone.

We need to find to handle it so that reopening the TaxEditor is not necessary anymore. And ofcourse it would be good to find out why it happens at all.

#18 Updated by Andreas Kohlbecker 6 months ago

  • Tags set to RemoteAccessException

#19 Updated by Andreas Kohlbecker 4 months ago

  • Description updated (diff)

Another report of the same issue:

Hallo,

der bulk editor ist nicht zugänglich, das ist die Fehlermeldung, die kommt.

viele Grüße
Nadja



last remote method : http://api.cybertaxonomy.org:80/caryophyllales_spp/remoting/reference.service
last remote request client time : 2020-02-19T15:46:47.448
last remote request response header time : Wed, 19 Feb 2020 15:46:47 GMT
client error time : 2020-02-19T15:46:47.507
login :
editor version : 5.12.0
server :  ()
schema version :
os : Windows 7 6.1 amd64
java : 1.8.0_20
org.springframework.remoting.RemoteAccessException: Could not access HTTP invoker remote service at [http://api.cybertaxonomy.org:80/caryophyllales_spp/remoting/reference.service]; nested exception is java.lang.NullPointerException
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.convertHttpInvokerAccessException(HttpInvokerClientInterceptor.java:216)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:147)
                at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
                at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
                at com.sun.proxy.$Proxy91.findByTitle(Unknown Source)
                at eu.etaxonomy.taxeditor.store.SearchManager.findReferences(SearchManager.java:97)
                at eu.etaxonomy.taxeditor.bulkeditor.input.ReferenceEditorInput.listEntities(ReferenceEditorInput.java:128)
                at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput.lambda$0(AbstractBulkEditorInput.java:280)
                at eu.etaxonomy.taxeditor.bulkeditor.input.AbstractBulkEditorInput$$Lambda$77/494966836.run(Unknown Source)
                at org.eclipse.core.runtime.jobs.Job$2.run(Job.java:186)
                at org.eclipse.core.internal.jobs.Worker.run(Worker.java:55)
Caused by: java.lang.NullPointerException
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getCacheElement(CdmTransientEntityCacher.java:288)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.getFromCache(CdmTransientEntityCacher.java:293)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.put(CdmTransientEntityCacher.java:261)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:268)
                at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:293)
                at eu.etaxonomy.cdm.cache.CacheLoader.loadRecursive(CacheLoader.java:84)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:184)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:155)
                at eu.etaxonomy.cdm.cache.CacheLoader.load(CacheLoader.java:69)
                at eu.etaxonomy.cdm.cache.CdmTransientEntityCacher.load(CdmTransientEntityCacher.java:157)
                at eu.etaxonomy.taxeditor.session.CdmEntitySession.load(CdmEntitySession.java:70)
                at eu.etaxonomy.taxeditor.session.CdmEntitySessionManager.load(CdmEntitySessionManager.java:131)
                at eu.etaxonomy.taxeditor.service.CdmServiceRequestExecutor.doExecuteRequest(CdmServiceRequestExecutor.java:82)
                at org.springframework.remoting.httpinvoker.AbstractHttpInvokerRequestExecutor.executeRequest(AbstractHttpInvokerRequestExecutor.java:138)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:194)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.executeRequest(HttpInvokerClientInterceptor.java:176)
                at org.springframework.remoting.httpinvoker.HttpInvokerClientInterceptor.invoke(HttpInvokerClientInterceptor.java:144)
                ... 9 more

#20 Updated by Andreas Kohlbecker 4 months ago

  • Tags deleted (RemoteAccessException)

#21 Updated by Andreas Kohlbecker 4 months ago

As far as I am understanding this problem it is the bulk editors which can be saves or closed while a search process is still running in a subthread. These sub threads are started in AbstractBulkEditorInput.performSearch(BulkEditorQuery bulkEditorQuery, IStructuredSelection selection)

In case of saving or even closing the editor the following code is execued:

public void save(IProgressMonitor monitor, boolean resetMerge) {
        ...
        input.dispose(); // calls internally cdmTransientEntityCacher.dispose();
        input.bind();
        ...
        if (lastQuery != null){
            // NOTE AK: what if a search is already in progress here?
            bulkEditorComposite.performSearch(lastQuery, selection);
        }
    }

    @PreDestroy
    public void dispose() {
            ...
        if(input!=null){
            input.dispose(); // calls internally cdmTransientEntityCacher.dispose();
        }
        ...
        }

The CachingHttpInvokerProxyFactoryBean (formerly CdmServiceRequestExecutor as shown in the stack traces here) will load the retuned entities into the cache by calling in principle

CdmApplicationState.getCurrentAppConfig()).getCdmEntitySessionManager().load()

which internally uses

 tlActiveSession.get().load(obj, update);

since tlActiveSession is an InheritableThreadLocal object the CdmEntitySession of the parent thread, that is of the bulkeditor thread is being accessed. The session could be disposed at this time, which means that cdmTransientEntityCacher.dispose(); has been executed and CdmTransientEntityCacher.getCache() will return null. This can happen either after a call to the BulkEditorE4.dispose() method or during BulkEditorE4.save() while being in the middle of dispose() and bind().

A solution to this problem would be to kill the search thread being in progress. This would require calling job.getThread().stop() since job.cancel() seems to wait for the job to gracefully shut down, which is not what we want in this case.

BTW: I also stumbled over these lines of code in .AbstractBulkEditorInput.performSearch(BulkEditorQuery bulkEditorQuery, IStructuredSelection selection):


    public void performSearch(final BulkEditorQuery bulkEditorQuery, IStructuredSelection selection) {
        //cancel previous search job
        if(searchJob!=null && searchJob.getState()!=Job.NONE){
            searchJob.cancel();
            searchJob = null;
            /*
             * wait for a little while for the job to finish
             * to avoid asynchronously loaded results of the
             * previous search being shown in the next search
             * (not critical but explicitly waiting for the job to finish
             * could run into an endless loop by mistake)
             */
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
            }
        }

This seems not to be right since sleeping for half a second is not always enough. Think about massive searches or low data rate connections! I t would be better to kill the thread as suggested above.

However, there is one point which does not yet fit into this picture. After this phenomenon has appeared it is needed to restart the editor. It should be sufficient to just close the bulkeditor to heal the situation.

#22 Updated by Katja Luther 4 months ago

  • Target version changed from Release 5.13 to Release 5.14

#23 Updated by Andreas Kohlbecker 3 months ago

  • Duplicated by bug #8923: NullPointerException (NPE) after attempt to save invalid Group name added

#24 Updated by Andreas Müller 3 months ago

  • Target version changed from Release 5.14 to Release 5.15

#25 Updated by Katja Luther 2 months ago

  • Target version changed from Release 5.15 to Release 5.17

Also available in: Atom PDF

Add picture from clipboard (Maximum size: 40 MB)