Commit ad34b9c278608dfdcfdbe7402acb3a6dd04c3d0e

Authored by Jay Berkenbilt
1 parent bf0e6eb3

Implement helpers for file attachments

ChangeLog
... ... @@ -4,6 +4,12 @@
4 4 pdf_time_to_qpdf_time, qpdf_time_to_pdf_time,
5 5 get_current_qpdf_time.
6 6  
  7 +2021-02-08 Jay Berkenbilt <ejb@ql.org>
  8 +
  9 + * Add helper classes for file attachments:
  10 + QPDFEmbeddedFileDocumentHelper, QPDFFileSpecObjectHelper,
  11 + QPDFEFStreamObjectHelper. See their header files for details.
  12 +
7 13 2021-02-07 Jay Berkenbilt <ejb@ql.org>
8 14  
9 15 * Add new functions QUtil::pipe_file and QUtil::file_provider for
... ...
include/qpdf/QPDFEFStreamObjectHelper.hh 0 → 100644
  1 +// Copyright (c) 2005-2021 Jay Berkenbilt
  2 +//
  3 +// This file is part of qpdf.
  4 +//
  5 +// Licensed under the Apache License, Version 2.0 (the "License");
  6 +// you may not use this file except in compliance with the License.
  7 +// You may obtain a copy of the License at
  8 +//
  9 +// http://www.apache.org/licenses/LICENSE-2.0
  10 +//
  11 +// Unless required by applicable law or agreed to in writing, software
  12 +// distributed under the License is distributed on an "AS IS" BASIS,
  13 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +// See the License for the specific language governing permissions and
  15 +// limitations under the License.
  16 +//
  17 +// Versions of qpdf prior to version 7 were released under the terms
  18 +// of version 2.0 of the Artistic License. At your option, you may
  19 +// continue to consider qpdf to be licensed under those terms. Please
  20 +// see the manual for additional information.
  21 +
  22 +#ifndef QPDFEFSTREAMOBJECTHELPER_HH
  23 +#define QPDFEFSTREAMOBJECTHELPER_HH
  24 +
  25 +#include <qpdf/QPDFObjectHelper.hh>
  26 +
  27 +#include <qpdf/DLL.h>
  28 +
  29 +#include <qpdf/QPDFObjectHandle.hh>
  30 +#include <functional>
  31 +
  32 +// This class provides a higher level interface around Embedded File
  33 +// Streams, which are discussed in section 7.11.4 of the ISO-32000 PDF
  34 +// specification.
  35 +
  36 +class QPDFEFStreamObjectHelper: public QPDFObjectHelper
  37 +{
  38 + public:
  39 + QPDF_DLL
  40 + QPDFEFStreamObjectHelper(QPDFObjectHandle);
  41 + QPDF_DLL
  42 + virtual ~QPDFEFStreamObjectHelper() = default;
  43 +
  44 + // Date parameters are strings that comform to the PDF spec for
  45 + // date/time strings, which is "D:yyyymmddhhmmss<z>" where <z> is
  46 + // either "Z" for UTC or "-hh'mm'" or "+hh'mm'" for timezone
  47 + // offset. Examples: "D:20210207161528-05'00'",
  48 + // "D:20210207211528Z". See QUtil::qpdf_time_to_pdf_time.
  49 +
  50 + QPDF_DLL
  51 + std::string getCreationDate();
  52 + QPDF_DLL
  53 + std::string getModDate();
  54 + // Get size as reported in the object; return 0 if not present.
  55 + QPDF_DLL
  56 + size_t getSize();
  57 + // Subtype is a mime type such as "text/plain"
  58 + QPDF_DLL
  59 + std::string getSubtype();
  60 + // Return the MD5 checksum as stored in the object as a binary
  61 + // string. This does not check consistency with the data. If not
  62 + // present, return an empty string.
  63 + QPDF_DLL
  64 + std::string getChecksum();
  65 +
  66 + // Setters return a reference to this object so that they can be
  67 + // used as fluent interfaces, e.g.
  68 + // efsoh.setCreationDate(x).setModDate(y);
  69 +
  70 + // Create a new embedded file stream with the given stream data,
  71 + // which can be provided in any of several ways. To get the new
  72 + // object back, call getObjectHandle() on the returned object. The
  73 + // checksum and size are computed automatically and stored. Other
  74 + // parameters may be supplied using setters defined below.
  75 + QPDF_DLL
  76 + static QPDFEFStreamObjectHelper
  77 + createEFStream(QPDF& qpdf, PointerHolder<Buffer> data);
  78 + QPDF_DLL
  79 + static QPDFEFStreamObjectHelper
  80 + createEFStream(QPDF& qpdf, std::string const& data);
  81 + // The provider function must write the data to the given
  82 + // pipeline. The function may be called multiple times by the qpdf
  83 + // library. You can pass QUtil::file_provider(filename) as the
  84 + // provider to have the qpdf library provide the contents of
  85 + // filename as a binary.
  86 + QPDF_DLL
  87 + static QPDFEFStreamObjectHelper
  88 + createEFStream(QPDF& qpdf, std::function<void(Pipeline*)> provider);
  89 +
  90 + // Setters for other parameters
  91 + QPDF_DLL
  92 + QPDFEFStreamObjectHelper& setCreationDate(std::string const&);
  93 + QPDF_DLL
  94 + QPDFEFStreamObjectHelper& setModDate(std::string const&);
  95 +
  96 + // Set subtype as a mime-type, e.g. "text/plain" or
  97 + // "application/pdf".
  98 + QPDF_DLL
  99 + QPDFEFStreamObjectHelper& setSubtype(std::string const&);
  100 +
  101 + private:
  102 + QPDFObjectHandle getParam(std::string const& pkey);
  103 + void setParam(std::string const& pkey, QPDFObjectHandle const&);
  104 + static QPDFEFStreamObjectHelper newFromStream(QPDFObjectHandle stream);
  105 +
  106 + class Members
  107 + {
  108 + friend class QPDFEFStreamObjectHelper;
  109 +
  110 + public:
  111 + QPDF_DLL
  112 + ~Members() = default;
  113 +
  114 + private:
  115 + Members();
  116 + Members(Members const&) = delete;
  117 + };
  118 +
  119 + PointerHolder<Members> m;
  120 +};
  121 +
  122 +#endif // QPDFEFSTREAMOBJECTHELPER_HH
... ...
include/qpdf/QPDFEmbeddedFileDocumentHelper.hh 0 → 100644
  1 +// Copyright (c) 2005-2021 Jay Berkenbilt
  2 +//
  3 +// This file is part of qpdf.
  4 +//
  5 +// Licensed under the Apache License, Version 2.0 (the "License");
  6 +// you may not use this file except in compliance with the License.
  7 +// You may obtain a copy of the License at
  8 +//
  9 +// http://www.apache.org/licenses/LICENSE-2.0
  10 +//
  11 +// Unless required by applicable law or agreed to in writing, software
  12 +// distributed under the License is distributed on an "AS IS" BASIS,
  13 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +// See the License for the specific language governing permissions and
  15 +// limitations under the License.
  16 +//
  17 +// Versions of qpdf prior to version 7 were released under the terms
  18 +// of version 2.0 of the Artistic License. At your option, you may
  19 +// continue to consider qpdf to be licensed under those terms. Please
  20 +// see the manual for additional information.
  21 +
  22 +#ifndef QPDFEMBEDDEDFILEDOCUMENTHELPER_HH
  23 +#define QPDFEMBEDDEDFILEDOCUMENTHELPER_HH
  24 +
  25 +#include <qpdf/QPDFDocumentHelper.hh>
  26 +
  27 +#include <qpdf/QPDF.hh>
  28 +#include <qpdf/QPDFNameTreeObjectHelper.hh>
  29 +#include <qpdf/QPDFFileSpecObjectHelper.hh>
  30 +#include <qpdf/DLL.h>
  31 +
  32 +#include <memory>
  33 +#include <map>
  34 +
  35 +// This class provides a higher level interface around document-level
  36 +// file attachments, also known as embedded files. These are discussed
  37 +// in sections 7.7.4 and 7.11 of the ISO-32000 PDF specification.
  38 +
  39 +class QPDFEmbeddedFileDocumentHelper: public QPDFDocumentHelper
  40 +{
  41 + public:
  42 + QPDF_DLL
  43 + QPDFEmbeddedFileDocumentHelper(QPDF&);
  44 + QPDF_DLL
  45 + virtual ~QPDFEmbeddedFileDocumentHelper() = default;
  46 +
  47 + QPDF_DLL
  48 + bool hasEmbeddedFiles() const;
  49 +
  50 + QPDF_DLL
  51 + std::map<std::string,
  52 + std::shared_ptr<QPDFFileSpecObjectHelper>> getEmbeddedFiles();
  53 +
  54 + // If an embedded file with the given name exists, return a
  55 + // (shared) pointer to it. Otherwise, return nullptr.
  56 + QPDF_DLL
  57 + std::shared_ptr<QPDFFileSpecObjectHelper>
  58 + getEmbeddedFile(std::string const& name);
  59 +
  60 + // Add or replace an attachment
  61 + QPDF_DLL
  62 + void replaceEmbeddedFile(
  63 + std::string const& name, QPDFFileSpecObjectHelper const&);
  64 +
  65 + // Remove an embedded file if present. Return value is true if the
  66 + // file was present and was removed. This method not only removes
  67 + // the embedded file from the embedded files name tree but also
  68 + // nulls out the file specification dictionary. This means that
  69 + // any references to this file from file attachment annotations
  70 + // will also stop working. This is the best way to make the
  71 + // attachment actually disappear from the file and not just from
  72 + // the list of attachments.
  73 + QPDF_DLL
  74 + bool removeEmbeddedFile(std::string const& name);
  75 +
  76 + private:
  77 + void initEmbeddedFiles();
  78 +
  79 + class Members
  80 + {
  81 + friend class QPDFEmbeddedFileDocumentHelper;
  82 +
  83 + public:
  84 + QPDF_DLL
  85 + ~Members() = default;
  86 +
  87 + private:
  88 + Members();
  89 + Members(Members const&) = delete;
  90 +
  91 + std::shared_ptr<QPDFNameTreeObjectHelper> embedded_files;
  92 + };
  93 +
  94 + PointerHolder<Members> m;
  95 +};
  96 +
  97 +#endif // QPDFEMBEDDEDFILEDOCUMENTHELPER_HH
... ...
include/qpdf/QPDFFileSpecObjectHelper.hh 0 → 100644
  1 +// Copyright (c) 2005-2021 Jay Berkenbilt
  2 +//
  3 +// This file is part of qpdf.
  4 +//
  5 +// Licensed under the Apache License, Version 2.0 (the "License");
  6 +// you may not use this file except in compliance with the License.
  7 +// You may obtain a copy of the License at
  8 +//
  9 +// http://www.apache.org/licenses/LICENSE-2.0
  10 +//
  11 +// Unless required by applicable law or agreed to in writing, software
  12 +// distributed under the License is distributed on an "AS IS" BASIS,
  13 +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14 +// See the License for the specific language governing permissions and
  15 +// limitations under the License.
  16 +//
  17 +// Versions of qpdf prior to version 7 were released under the terms
  18 +// of version 2.0 of the Artistic License. At your option, you may
  19 +// continue to consider qpdf to be licensed under those terms. Please
  20 +// see the manual for additional information.
  21 +
  22 +#ifndef QPDFFILESPECOBJECTHELPER_HH
  23 +#define QPDFFILESPECOBJECTHELPER_HH
  24 +
  25 +#include <qpdf/QPDFObjectHelper.hh>
  26 +
  27 +#include <qpdf/DLL.h>
  28 +
  29 +#include <qpdf/QPDFObjectHandle.hh>
  30 +#include <qpdf/QPDFEFStreamObjectHelper.hh>
  31 +
  32 +// This class provides a higher level interface around File
  33 +// Specification dictionaries, which are discussed in section 7.11 of
  34 +// the ISO-32000 PDF specification.
  35 +
  36 +class QPDFFileSpecObjectHelper: public QPDFObjectHelper
  37 +{
  38 + public:
  39 + QPDF_DLL
  40 + QPDFFileSpecObjectHelper(QPDFObjectHandle);
  41 + QPDF_DLL
  42 + virtual ~QPDFFileSpecObjectHelper() = default;
  43 +
  44 + QPDF_DLL
  45 + std::string getDescription();
  46 +
  47 + // Get the main filename for this file specification. In priority
  48 + // order, check /UF, /F, /Unix, /DOS, /Mac.
  49 + QPDF_DLL
  50 + std::string getFilename();
  51 +
  52 + // Return any of /UF, /F, /Unix, /DOS, /Mac filename keys that may
  53 + // be present in the object.
  54 + QPDF_DLL
  55 + std::map<std::string, std::string> getFilenames();
  56 +
  57 + // Get the requested embedded file stream for this file
  58 + // specification. If key is empty, In priority order, check /UF,
  59 + // /F, /Unix, /DOS, /Mac. Returns a null object if not found. If
  60 + // this is an actual embedded file stream, its data is the content
  61 + // of the attachment. You can also use
  62 + // QPDFEFStreamObjectHelper for higher level access to
  63 + // the parameters.
  64 + QPDF_DLL
  65 + QPDFObjectHandle getEmbeddedFileStream(std::string const& key = "");
  66 +
  67 + // Return the /EF key of the file spec, which is a map from file
  68 + // name key to embedded file stream.
  69 + QPDF_DLL
  70 + QPDFObjectHandle getEmbeddedFileStreams();
  71 +
  72 + // Setters return a reference to this object so that they can be
  73 + // used as fluent interfaces, e.g.
  74 + // fsoh.setDescription(x).setFilename(y);
  75 +
  76 + // Create a new filespec as an indirect object with the given
  77 + // filename, and attach the contents of the specified file as data
  78 + // in an embedded file stream.
  79 + QPDF_DLL
  80 + static
  81 + QPDFFileSpecObjectHelper createFileSpec(
  82 + QPDF& qpdf,
  83 + std::string const& filename,
  84 + std::string const& fullpath);
  85 +
  86 + // Create a new filespec as an indirect object with the given
  87 + // unicode filename and embedded file stream. The file name will
  88 + // be used as both /UF and /F. If you need to override, call
  89 + // setFilename.
  90 + QPDF_DLL
  91 + static
  92 + QPDFFileSpecObjectHelper createFileSpec(
  93 + QPDF& qpdf,
  94 + std::string const& filename,
  95 + QPDFEFStreamObjectHelper);
  96 +
  97 + QPDF_DLL
  98 + QPDFFileSpecObjectHelper& setDescription(std::string const&);
  99 + // setFilename sets /UF to unicode_name. If compat_name is empty,
  100 + // it is also set to unicode_name. unicode_name should be a UTF-8
  101 + // encoded string. compat_name is converted to a string
  102 + // QPDFObjectHandle literally, preserving whatever encoding it
  103 + // might happen to have.
  104 + QPDF_DLL
  105 + QPDFFileSpecObjectHelper& setFilename(
  106 + std::string const& unicode_name,
  107 + std::string const& compat_name = "");
  108 +
  109 + private:
  110 + class Members
  111 + {
  112 + friend class QPDFFileSpecObjectHelper;
  113 +
  114 + public:
  115 + QPDF_DLL
  116 + ~Members() = default;
  117 +
  118 + private:
  119 + Members();
  120 + Members(Members const&) = delete;
  121 + };
  122 +
  123 + PointerHolder<Members> m;
  124 +};
  125 +
  126 +#endif // QPDFFILESPECOBJECTHELPER_HH
... ...
libqpdf/QPDFEFStreamObjectHelper.cc 0 → 100644
  1 +#include <qpdf/QPDFEFStreamObjectHelper.hh>
  2 +#include <qpdf/QIntC.hh>
  3 +#include <qpdf/QUtil.hh>
  4 +#include <qpdf/Pl_Count.hh>
  5 +#include <qpdf/Pl_MD5.hh>
  6 +#include <qpdf/Pl_Discard.hh>
  7 +
  8 +QPDFEFStreamObjectHelper::QPDFEFStreamObjectHelper(
  9 + QPDFObjectHandle oh) :
  10 + QPDFObjectHelper(oh),
  11 + m(new Members())
  12 +{
  13 +}
  14 +
  15 +QPDFEFStreamObjectHelper::Members::Members()
  16 +{
  17 +}
  18 +
  19 +QPDFObjectHandle
  20 +QPDFEFStreamObjectHelper::getParam(std::string const& pkey)
  21 +{
  22 + auto params = this->oh.getDict().getKey("/Params");
  23 + if (params.isDictionary())
  24 + {
  25 + return params.getKey(pkey);
  26 + }
  27 + return QPDFObjectHandle::newNull();
  28 +}
  29 +
  30 +void
  31 +QPDFEFStreamObjectHelper::setParam(
  32 + std::string const& pkey, QPDFObjectHandle const& pval)
  33 +{
  34 + auto params = this->oh.getDict().getKey("/Params");
  35 + if (! params.isDictionary())
  36 + {
  37 + params = QPDFObjectHandle::newDictionary();
  38 + this->oh.getDict().replaceKey("/Params", params);
  39 + }
  40 + params.replaceKey(pkey, pval);
  41 +}
  42 +
  43 +std::string
  44 +QPDFEFStreamObjectHelper::getCreationDate()
  45 +{
  46 + auto val = getParam("/CreationDate");
  47 + if (val.isString())
  48 + {
  49 + return val.getUTF8Value();
  50 + }
  51 + return "";
  52 +}
  53 +
  54 +std::string
  55 +QPDFEFStreamObjectHelper::getModDate()
  56 +{
  57 + auto val = getParam("/ModDate");
  58 + if (val.isString())
  59 + {
  60 + return val.getUTF8Value();
  61 + }
  62 + return "";
  63 +}
  64 +
  65 +size_t
  66 +QPDFEFStreamObjectHelper::getSize()
  67 +{
  68 + auto val = getParam("/Size");
  69 + if (val.isInteger())
  70 + {
  71 + return QIntC::to_size(val.getUIntValueAsUInt());
  72 + }
  73 + return 0;
  74 +}
  75 +
  76 +std::string
  77 +QPDFEFStreamObjectHelper::getSubtype()
  78 +{
  79 + auto val = getParam("/Subtype");
  80 + if (val.isName())
  81 + {
  82 + auto n = val.getName();
  83 + if (n.length() > 1)
  84 + {
  85 + return n.substr(1);
  86 + }
  87 + }
  88 + return "";
  89 +}
  90 +
  91 +std::string
  92 +QPDFEFStreamObjectHelper::getChecksum()
  93 +{
  94 + auto val = getParam("/CheckSum");
  95 + if (val.isString())
  96 + {
  97 + return val.getStringValue();
  98 + }
  99 + return "";
  100 +}
  101 +
  102 +QPDFEFStreamObjectHelper
  103 +QPDFEFStreamObjectHelper::createEFStream(
  104 + QPDF& qpdf, PointerHolder<Buffer> data)
  105 +{
  106 + return newFromStream(QPDFObjectHandle::newStream(&qpdf, data));
  107 +}
  108 +
  109 +QPDFEFStreamObjectHelper
  110 +QPDFEFStreamObjectHelper::createEFStream(
  111 + QPDF& qpdf, std::string const& data)
  112 +{
  113 + return newFromStream(QPDFObjectHandle::newStream(&qpdf, data));
  114 +}
  115 +
  116 +namespace QEF
  117 +{
  118 + class Provider: public QPDFObjectHandle::StreamDataProvider
  119 + {
  120 + public:
  121 + Provider(std::function<void(Pipeline*)> provider) :
  122 + StreamDataProvider(false),
  123 + provider(provider)
  124 + {
  125 + }
  126 + virtual ~Provider() = default;
  127 + virtual void provideStreamData(int objid, int generation,
  128 + Pipeline* pipeline) override
  129 + {
  130 + this->provider(pipeline);
  131 + }
  132 +
  133 + private:
  134 + std::function<void(Pipeline*)> provider;
  135 + };
  136 +};
  137 +
  138 +QPDFEFStreamObjectHelper
  139 +QPDFEFStreamObjectHelper::createEFStream(
  140 + QPDF& qpdf, std::function<void(Pipeline*)> provider)
  141 +{
  142 + auto stream = QPDFObjectHandle::newStream(&qpdf);
  143 + stream.replaceStreamData(new QEF::Provider(provider),
  144 + QPDFObjectHandle::newNull(),
  145 + QPDFObjectHandle::newNull());
  146 + return newFromStream(stream);
  147 +}
  148 +
  149 +QPDFEFStreamObjectHelper&
  150 +QPDFEFStreamObjectHelper::setCreationDate(std::string const& date)
  151 +{
  152 + setParam("/CreationDate", QPDFObjectHandle::newString(date));
  153 + return *this;
  154 +}
  155 +
  156 +QPDFEFStreamObjectHelper&
  157 +QPDFEFStreamObjectHelper::setModDate(std::string const& date)
  158 +{
  159 + setParam("/ModDate", QPDFObjectHandle::newString(date));
  160 + return *this;
  161 +}
  162 +
  163 +QPDFEFStreamObjectHelper&
  164 +QPDFEFStreamObjectHelper::setSubtype(std::string const& subtype)
  165 +{
  166 + setParam("/Subtype", QPDFObjectHandle::newName("/" + subtype));
  167 + return *this;
  168 +}
  169 +
  170 +QPDFEFStreamObjectHelper
  171 +QPDFEFStreamObjectHelper::newFromStream(QPDFObjectHandle stream)
  172 +{
  173 + QPDFEFStreamObjectHelper result(stream);
  174 + stream.getDict().replaceKey(
  175 + "/Type", QPDFObjectHandle::newName("/EmbeddedFile"));
  176 + Pl_Discard discard;
  177 + Pl_MD5 md5("EF md5", &discard);
  178 + Pl_Count count("EF size", &md5);
  179 + if (! stream.pipeStreamData(&count, nullptr, 0, qpdf_dl_all))
  180 + {
  181 + stream.warnIfPossible(
  182 + "unable to get stream data for new embedded file stream");
  183 + }
  184 + else
  185 + {
  186 + result.setParam(
  187 + "/Size", QPDFObjectHandle::newInteger(count.getCount()));
  188 + result.setParam(
  189 + "/CheckSum", QPDFObjectHandle::newString(
  190 + QUtil::hex_decode(md5.getHexDigest())));
  191 + }
  192 + return result;
  193 +}
... ...
libqpdf/QPDFEmbeddedFileDocumentHelper.cc 0 → 100644
  1 +#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
  2 +
  3 +// File attachments are stored in the /EmbeddedFiles (name tree) key
  4 +// of the /Names dictionary from the document catalog. Each entry
  5 +// points to a /FileSpec, which in turn points to one more Embedded
  6 +// File Streams. Note that file specs can appear in other places as
  7 +// well, such as file attachment annotations, among others.
  8 +//
  9 +// root -> /Names -> /EmbeddedFiles = name tree
  10 +// filename -> filespec
  11 +// <<
  12 +// /Desc ()
  13 +// /EF <<
  14 +// /F x 0 R
  15 +// /UF x 0 R
  16 +// >>
  17 +// /F (name)
  18 +// /UF (name)
  19 +// /Type /Filespec
  20 +// >>
  21 +// x 0 obj
  22 +// <<
  23 +// /Type /EmbeddedFile
  24 +// /DL filesize % not in spec?
  25 +// /Params <<
  26 +// /CheckSum <md5>
  27 +// /CreationDate (D:yyyymmddhhmmss{-hh'mm'|+hh'mm'|Z})
  28 +// /ModDate (D:yyyymmddhhmmss-hh'mm')
  29 +// /Size filesize
  30 +// /Subtype /mime#2ftype
  31 +// >>
  32 +// >>
  33 +
  34 +QPDFEmbeddedFileDocumentHelper::QPDFEmbeddedFileDocumentHelper(QPDF& qpdf) :
  35 + QPDFDocumentHelper(qpdf),
  36 + m(new Members())
  37 +{
  38 + auto root = qpdf.getRoot();
  39 + auto names = root.getKey("/Names");
  40 + if (names.isDictionary())
  41 + {
  42 + auto embedded_files = names.getKey("/EmbeddedFiles");
  43 + if (embedded_files.isDictionary())
  44 + {
  45 + this->m->embedded_files =
  46 + std::make_shared<QPDFNameTreeObjectHelper>(
  47 + embedded_files, qpdf);
  48 + }
  49 + }
  50 +}
  51 +
  52 +QPDFEmbeddedFileDocumentHelper::Members::Members()
  53 +{
  54 +}
  55 +
  56 +bool
  57 +QPDFEmbeddedFileDocumentHelper::hasEmbeddedFiles() const
  58 +{
  59 + return (this->m->embedded_files.get() != nullptr);
  60 +}
  61 +
  62 +void
  63 +QPDFEmbeddedFileDocumentHelper::initEmbeddedFiles()
  64 +{
  65 + if (hasEmbeddedFiles())
  66 + {
  67 + return;
  68 + }
  69 + auto root = qpdf.getRoot();
  70 + auto names = root.getKey("/Names");
  71 + if (! names.isDictionary())
  72 + {
  73 + names = QPDFObjectHandle::newDictionary();
  74 + root.replaceKey("/Names", names);
  75 + }
  76 + auto embedded_files = names.getKey("/EmbeddedFiles");
  77 + if (! embedded_files.isDictionary())
  78 + {
  79 + auto nth = QPDFNameTreeObjectHelper::newEmpty(this->qpdf);
  80 + names.replaceKey("/EmbeddedFiles", nth.getObjectHandle());
  81 + this->m->embedded_files =
  82 + std::make_shared<QPDFNameTreeObjectHelper>(nth);
  83 + }
  84 +}
  85 +
  86 +std::shared_ptr<QPDFFileSpecObjectHelper>
  87 +QPDFEmbeddedFileDocumentHelper::getEmbeddedFile(std::string const& name)
  88 +{
  89 + std::shared_ptr<QPDFFileSpecObjectHelper> result;
  90 + if (this->m->embedded_files)
  91 + {
  92 + auto i = this->m->embedded_files->find(name);
  93 + if (i != this->m->embedded_files->end())
  94 + {
  95 + result = std::make_shared<QPDFFileSpecObjectHelper>(i->second);
  96 + }
  97 + }
  98 + return result;
  99 +}
  100 +
  101 +std::map<std::string, std::shared_ptr<QPDFFileSpecObjectHelper>>
  102 +QPDFEmbeddedFileDocumentHelper::getEmbeddedFiles()
  103 +{
  104 + std::map<std::string,
  105 + std::shared_ptr<QPDFFileSpecObjectHelper>> result;
  106 + if (this->m->embedded_files)
  107 + {
  108 + for (auto const& i: *(this->m->embedded_files))
  109 + {
  110 + result[i.first] = std::make_shared<QPDFFileSpecObjectHelper>(
  111 + i.second);
  112 + }
  113 + }
  114 + return result;
  115 +}
  116 +
  117 +void
  118 +QPDFEmbeddedFileDocumentHelper::replaceEmbeddedFile(
  119 + std::string const& name, QPDFFileSpecObjectHelper const& fs)
  120 +{
  121 + initEmbeddedFiles();
  122 + this->m->embedded_files->insert(
  123 + name, fs.getObjectHandle());
  124 +}
  125 +
  126 +bool
  127 +QPDFEmbeddedFileDocumentHelper::removeEmbeddedFile(std::string const& name)
  128 +{
  129 + if (! hasEmbeddedFiles())
  130 + {
  131 + return false;
  132 + }
  133 + auto iter = this->m->embedded_files->find(name);
  134 + if (iter == this->m->embedded_files->end())
  135 + {
  136 + return false;
  137 + }
  138 + auto oh = iter->second;
  139 + iter.remove();
  140 + if (oh.isIndirect())
  141 + {
  142 + this->qpdf.replaceObject(oh.getObjGen(), QPDFObjectHandle::newNull());
  143 + }
  144 +
  145 + return true;
  146 +}
... ...
libqpdf/QPDFFileSpecObjectHelper.cc 0 → 100644
  1 +#include <qpdf/QPDFFileSpecObjectHelper.hh>
  2 +#include <qpdf/QTC.hh>
  3 +#include <qpdf/QPDF.hh>
  4 +#include <qpdf/QUtil.hh>
  5 +
  6 +#include <vector>
  7 +#include <string>
  8 +
  9 +QPDFFileSpecObjectHelper::QPDFFileSpecObjectHelper(
  10 + QPDFObjectHandle oh) :
  11 + QPDFObjectHelper(oh)
  12 +{
  13 + if (! oh.isDictionary())
  14 + {
  15 + oh.warnIfPossible("Embedded file object is not a dictionary");
  16 + return;
  17 + }
  18 + auto type = oh.getKey("/Type");
  19 + if (! (type.isName() && (type.getName() == "/Filespec")))
  20 + {
  21 + oh.warnIfPossible("Embedded file object's type is not /Filespec");
  22 + }
  23 +}
  24 +
  25 +QPDFFileSpecObjectHelper::Members::Members()
  26 +{
  27 +}
  28 +
  29 +static std::vector<std::string> name_keys = {
  30 + "/UF", "/F", "/Unix", "/DOS", "/Mac"};
  31 +
  32 +std::string
  33 +QPDFFileSpecObjectHelper::getDescription()
  34 +{
  35 + std::string result;
  36 + auto desc = this->oh.getKey("/Desc");
  37 + if (desc.isString())
  38 + {
  39 + result = desc.getUTF8Value();
  40 + }
  41 + return result;
  42 +}
  43 +
  44 +std::string
  45 +QPDFFileSpecObjectHelper::getFilename()
  46 +{
  47 + for (auto const& i: name_keys)
  48 + {
  49 + auto k = this->oh.getKey(i);
  50 + if (k.isString())
  51 + {
  52 + return k.getUTF8Value();
  53 + }
  54 + }
  55 + return "";
  56 +}
  57 +
  58 +std::map<std::string, std::string>
  59 +QPDFFileSpecObjectHelper::getFilenames()
  60 +{
  61 + std::map<std::string, std::string> result;
  62 + for (auto const& i: name_keys)
  63 + {
  64 + auto k = this->oh.getKey(i);
  65 + if (k.isString())
  66 + {
  67 + result[i] = k.getUTF8Value();
  68 + }
  69 + }
  70 + return result;
  71 +}
  72 +
  73 +QPDFObjectHandle
  74 +QPDFFileSpecObjectHelper::getEmbeddedFileStream(std::string const& key)
  75 +{
  76 + auto ef = this->oh.getKey("/EF");
  77 + if (! ef.isDictionary())
  78 + {
  79 + return QPDFObjectHandle::newNull();
  80 + }
  81 + if (! key.empty())
  82 + {
  83 + return ef.getKey(key);
  84 + }
  85 + for (auto const& i: name_keys)
  86 + {
  87 + auto k = ef.getKey(i);
  88 + if (k.isStream())
  89 + {
  90 + return k;
  91 + }
  92 + }
  93 + return QPDFObjectHandle::newNull();
  94 +}
  95 +
  96 +QPDFObjectHandle
  97 +QPDFFileSpecObjectHelper::getEmbeddedFileStreams()
  98 +{
  99 + return this->oh.getKey("/EF");
  100 +}
  101 +
  102 +QPDFFileSpecObjectHelper
  103 +QPDFFileSpecObjectHelper::createFileSpec(
  104 + QPDF& qpdf,
  105 + std::string const& filename,
  106 + std::string const& fullpath)
  107 +{
  108 + return createFileSpec(
  109 + qpdf, filename,
  110 + QPDFEFStreamObjectHelper::createEFStream(
  111 + qpdf,
  112 + QUtil::file_provider(fullpath)));
  113 +}
  114 +
  115 +QPDFFileSpecObjectHelper
  116 +QPDFFileSpecObjectHelper::createFileSpec(
  117 + QPDF& qpdf,
  118 + std::string const& filename,
  119 + QPDFEFStreamObjectHelper efsoh)
  120 +{
  121 + auto oh = qpdf.makeIndirectObject(QPDFObjectHandle::newDictionary());
  122 + oh.replaceKey("/Type", QPDFObjectHandle::newName("/Filespec"));
  123 + QPDFFileSpecObjectHelper result(oh);
  124 + result.setFilename(filename);
  125 + auto ef = QPDFObjectHandle::newDictionary();
  126 + ef.replaceKey("/F", efsoh.getObjectHandle());
  127 + ef.replaceKey("/UF", efsoh.getObjectHandle());
  128 + oh.replaceKey("/EF", ef);
  129 + return result;
  130 +}
  131 +
  132 +QPDFFileSpecObjectHelper&
  133 +QPDFFileSpecObjectHelper::setDescription(std::string const& desc)
  134 +{
  135 + this->oh.replaceKey("/Desc", QPDFObjectHandle::newUnicodeString(desc));
  136 + return *this;
  137 +}
  138 +
  139 +QPDFFileSpecObjectHelper&
  140 +QPDFFileSpecObjectHelper::setFilename(
  141 + std::string const& unicode_name,
  142 + std::string const& compat_name)
  143 +{
  144 + auto uf = QPDFObjectHandle::newUnicodeString(unicode_name);
  145 + this->oh.replaceKey("/UF", uf);
  146 + if (compat_name.empty())
  147 + {
  148 + QTC::TC("qpdf", "QPDFFileSpecObjectHelper empty compat_name");
  149 + this->oh.replaceKey("/F", uf);
  150 + }
  151 + else
  152 + {
  153 + QTC::TC("qpdf", "QPDFFileSpecObjectHelper non-empty compat_name");
  154 + this->oh.replaceKey("/F", QPDFObjectHandle::newString(compat_name));
  155 + }
  156 + return *this;
  157 +}
... ...
libqpdf/build.mk
... ... @@ -58,7 +58,10 @@ SRCS_libqpdf = \
58 58 libqpdf/QPDFAcroFormDocumentHelper.cc \
59 59 libqpdf/QPDFAnnotationObjectHelper.cc \
60 60 libqpdf/QPDFCryptoProvider.cc \
  61 + libqpdf/QPDFEFStreamObjectHelper.cc \
  62 + libqpdf/QPDFEmbeddedFileDocumentHelper.cc \
61 63 libqpdf/QPDFExc.cc \
  64 + libqpdf/QPDFFileSpecObjectHelper.cc \
62 65 libqpdf/QPDFFormFieldObjectHelper.cc \
63 66 libqpdf/QPDFMatrix.cc \
64 67 libqpdf/QPDFNameTreeObjectHelper.cc \
... ...
manual/qpdf-manual.xml
... ... @@ -4951,6 +4951,16 @@ print &quot;\n&quot;;
4951 4951 </listitem>
4952 4952 <listitem>
4953 4953 <para>
  4954 + Add new helper classes for supporting file attachments, also
  4955 + known as embedded files. New classes are
  4956 + <classname>QPDFEmbeddedFileDocumentHelper</classname>,
  4957 + <classname>QPDFFileSpecObjectHelper</classname>, and
  4958 + <classname>QPDFEFStreamObjectHelper</classname>. See their
  4959 + respective headers for details.
  4960 + </para>
  4961 + </listitem>
  4962 + <listitem>
  4963 + <para>
4954 4964 Add <function>warn</function> to
4955 4965 <classname>QPDF</classname>'s public API.
4956 4966 </para>
... ...
qpdf/qpdf.testcov
... ... @@ -569,3 +569,5 @@ QPDFPageObjectHelper unresolved names 0
569 569 QPDFPageObjectHelper resolving unresolved 0
570 570 qpdf password stdin 0
571 571 qpdf password file 0
  572 +QPDFFileSpecObjectHelper empty compat_name 0
  573 +QPDFFileSpecObjectHelper non-empty compat_name 0
... ...
qpdf/qtest/qpdf.test
... ... @@ -522,6 +522,29 @@ $td-&gt;runtest(&quot;page operations on form xobject&quot;,
522 522  
523 523 show_ntests();
524 524 # ----------
  525 +$td->notify("--- File Attachments ---");
  526 +$n_tests += 4;
  527 +
  528 +open(F, ">auto-txt") or die;
  529 +print F "from file";
  530 +close(F);
  531 +$td->runtest("attachments",
  532 + {$td->COMMAND => "test_driver 76 minimal.pdf auto-txt"},
  533 + {$td->FILE => "test76.out", $td->EXIT_STATUS => 0},
  534 + $td->NORMALIZE_NEWLINES);
  535 +$td->runtest("check output",
  536 + {$td->FILE => "a.pdf"},
  537 + {$td->FILE => "test76.pdf"});
  538 +$td->runtest("attachments",
  539 + {$td->COMMAND => "test_driver 77 test76.pdf"},
  540 + {$td->STRING => "test 77 done\n", $td->EXIT_STATUS => 0},
  541 + $td->NORMALIZE_NEWLINES);
  542 +$td->runtest("check output",
  543 + {$td->FILE => "a.pdf"},
  544 + {$td->FILE => "test77.pdf"});
  545 +
  546 +show_ntests();
  547 +# ----------
525 548 $td->notify("--- Stream Replacement Tests ---");
526 549 $n_tests += 8;
527 550  
... ...
qpdf/qtest/qpdf/test76.out 0 → 100644
  1 +att1 -> att1.txt
  2 +att2 -> att2.txt
  3 +att3 -> π.txt
  4 +test 76 done
... ...
qpdf/qtest/qpdf/test76.pdf 0 → 100644
  1 +%PDF-1.3
  2 +%¿÷¢þ
  3 +%QDF-1.0
  4 +
  5 +%% Original object ID: 1 0
  6 +1 0 obj
  7 +<<
  8 + /Names <<
  9 + /EmbeddedFiles 2 0 R
  10 + >>
  11 + /Pages 3 0 R
  12 + /Type /Catalog
  13 +>>
  14 +endobj
  15 +
  16 +%% Original object ID: 9 0
  17 +2 0 obj
  18 +<<
  19 + /Names [
  20 + (att1)
  21 + 4 0 R
  22 + (att2)
  23 + 5 0 R
  24 + (att3)
  25 + 6 0 R
  26 + ]
  27 +>>
  28 +endobj
  29 +
  30 +%% Original object ID: 2 0
  31 +3 0 obj
  32 +<<
  33 + /Count 1
  34 + /Kids [
  35 + 7 0 R
  36 + ]
  37 + /Type /Pages
  38 +>>
  39 +endobj
  40 +
  41 +%% Original object ID: 8 0
  42 +4 0 obj
  43 +<<
  44 + /Desc (some text)
  45 + /EF <<
  46 + /F 8 0 R
  47 + /UF 8 0 R
  48 + >>
  49 + /F (att1.txt)
  50 + /Type /Filespec
  51 + /UF (att1.txt)
  52 +>>
  53 +endobj
  54 +
  55 +%% Original object ID: 12 0
  56 +5 0 obj
  57 +<<
  58 + /EF <<
  59 + /F 10 0 R
  60 + /UF 10 0 R
  61 + >>
  62 + /F (att2.txt)
  63 + /Type /Filespec
  64 + /UF (att2.txt)
  65 +>>
  66 +endobj
  67 +
  68 +%% Original object ID: 13 0
  69 +6 0 obj
  70 +<<
  71 + /EF <<
  72 + /F 12 0 R
  73 + /UF 12 0 R
  74 + >>
  75 + /F (att3.txt)
  76 + /Type /Filespec
  77 + /UF <feff03c0002e007400780074>
  78 +>>
  79 +endobj
  80 +
  81 +%% Page 1
  82 +%% Original object ID: 3 0
  83 +7 0 obj
  84 +<<
  85 + /Contents 14 0 R
  86 + /MediaBox [
  87 + 0
  88 + 0
  89 + 612
  90 + 792
  91 + ]
  92 + /Parent 3 0 R
  93 + /Resources <<
  94 + /Font <<
  95 + /F1 16 0 R
  96 + >>
  97 + /ProcSet 17 0 R
  98 + >>
  99 + /Type /Page
  100 +>>
  101 +endobj
  102 +
  103 +%% Original object ID: 7 0
  104 +8 0 obj
  105 +<<
  106 + /Params <<
  107 + /CheckSum <2e10f186a4cdf5be438747f4bdc2d4d4>
  108 + /CreationDate (D:20210207191121-05'00')
  109 + /ModDate (D:20210208001122Z)
  110 + /Size 9
  111 + /Subtype /text#2fplain
  112 + >>
  113 + /Type /EmbeddedFile
  114 + /Length 9 0 R
  115 +>>
  116 +stream
  117 +from file
  118 +endstream
  119 +endobj
  120 +
  121 +%QDF: ignore_newline
  122 +9 0 obj
  123 +9
  124 +endobj
  125 +
  126 +%% Original object ID: 10 0
  127 +10 0 obj
  128 +<<
  129 + /Params <<
  130 + /CheckSum <2fce9c8228e360ba9b04a1bd1bf63d6b>
  131 + /Size 11
  132 + /Subtype /text#2fplain
  133 + >>
  134 + /Type /EmbeddedFile
  135 + /Length 11 0 R
  136 +>>
  137 +stream
  138 +from string
  139 +endstream
  140 +endobj
  141 +
  142 +%QDF: ignore_newline
  143 +11 0 obj
  144 +11
  145 +endobj
  146 +
  147 +%% Original object ID: 11 0
  148 +12 0 obj
  149 +<<
  150 + /Params <<
  151 + /CheckSum <2236c155b1d62b7f00285bba081d4336>
  152 + /Size 11
  153 + /Subtype /text#2fplain
  154 + >>
  155 + /Type /EmbeddedFile
  156 + /Length 13 0 R
  157 +>>
  158 +stream
  159 +from buffer
  160 +endstream
  161 +endobj
  162 +
  163 +%QDF: ignore_newline
  164 +13 0 obj
  165 +11
  166 +endobj
  167 +
  168 +%% Contents for page 1
  169 +%% Original object ID: 4 0
  170 +14 0 obj
  171 +<<
  172 + /Length 15 0 R
  173 +>>
  174 +stream
  175 +BT
  176 + /F1 24 Tf
  177 + 72 720 Td
  178 + (Potato) Tj
  179 +ET
  180 +endstream
  181 +endobj
  182 +
  183 +15 0 obj
  184 +44
  185 +endobj
  186 +
  187 +%% Original object ID: 6 0
  188 +16 0 obj
  189 +<<
  190 + /BaseFont /Helvetica
  191 + /Encoding /WinAnsiEncoding
  192 + /Name /F1
  193 + /Subtype /Type1
  194 + /Type /Font
  195 +>>
  196 +endobj
  197 +
  198 +%% Original object ID: 5 0
  199 +17 0 obj
  200 +[
  201 + /PDF
  202 + /Text
  203 +]
  204 +endobj
  205 +
  206 +xref
  207 +0 18
  208 +0000000000 65535 f
  209 +0000000052 00000 n
  210 +0000000175 00000 n
  211 +0000000302 00000 n
  212 +0000000401 00000 n
  213 +0000000563 00000 n
  214 +0000000707 00000 n
  215 +0000000876 00000 n
  216 +0000001098 00000 n
  217 +0000001389 00000 n
  218 +0000001435 00000 n
  219 +0000001654 00000 n
  220 +0000001702 00000 n
  221 +0000001921 00000 n
  222 +0000001991 00000 n
  223 +0000002092 00000 n
  224 +0000002139 00000 n
  225 +0000002285 00000 n
  226 +trailer <<
  227 + /Root 1 0 R
  228 + /Size 18
  229 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>]
  230 +>>
  231 +startxref
  232 +2321
  233 +%%EOF
... ...
qpdf/qtest/qpdf/test77.pdf 0 → 100644
  1 +%PDF-1.3
  2 +%¿÷¢þ
  3 +%QDF-1.0
  4 +
  5 +%% Original object ID: 1 0
  6 +1 0 obj
  7 +<<
  8 + /Names <<
  9 + /EmbeddedFiles 2 0 R
  10 + >>
  11 + /Pages 3 0 R
  12 + /Type /Catalog
  13 +>>
  14 +endobj
  15 +
  16 +%% Original object ID: 2 0
  17 +2 0 obj
  18 +<<
  19 + /Names [
  20 + (att1)
  21 + 4 0 R
  22 + (att3)
  23 + 5 0 R
  24 + ]
  25 +>>
  26 +endobj
  27 +
  28 +%% Original object ID: 3 0
  29 +3 0 obj
  30 +<<
  31 + /Count 1
  32 + /Kids [
  33 + 6 0 R
  34 + ]
  35 + /Type /Pages
  36 +>>
  37 +endobj
  38 +
  39 +%% Original object ID: 4 0
  40 +4 0 obj
  41 +<<
  42 + /Desc (some text)
  43 + /EF <<
  44 + /F 7 0 R
  45 + /UF 7 0 R
  46 + >>
  47 + /F (att1.txt)
  48 + /Type /Filespec
  49 + /UF (att1.txt)
  50 +>>
  51 +endobj
  52 +
  53 +%% Original object ID: 6 0
  54 +5 0 obj
  55 +<<
  56 + /EF <<
  57 + /F 9 0 R
  58 + /UF 9 0 R
  59 + >>
  60 + /F (att3.txt)
  61 + /Type /Filespec
  62 + /UF <feff03c0002e007400780074>
  63 +>>
  64 +endobj
  65 +
  66 +%% Page 1
  67 +%% Original object ID: 7 0
  68 +6 0 obj
  69 +<<
  70 + /Contents 11 0 R
  71 + /MediaBox [
  72 + 0
  73 + 0
  74 + 612
  75 + 792
  76 + ]
  77 + /Parent 3 0 R
  78 + /Resources <<
  79 + /Font <<
  80 + /F1 13 0 R
  81 + >>
  82 + /ProcSet 14 0 R
  83 + >>
  84 + /Type /Page
  85 +>>
  86 +endobj
  87 +
  88 +%% Original object ID: 8 0
  89 +7 0 obj
  90 +<<
  91 + /Params <<
  92 + /CheckSum <2e10f186a4cdf5be438747f4bdc2d4d4>
  93 + /CreationDate (D:20210207191121-05'00')
  94 + /ModDate (D:20210208001122Z)
  95 + /Size 9
  96 + /Subtype /text#2fplain
  97 + >>
  98 + /Type /EmbeddedFile
  99 + /Length 8 0 R
  100 +>>
  101 +stream
  102 +from file
  103 +endstream
  104 +endobj
  105 +
  106 +%QDF: ignore_newline
  107 +8 0 obj
  108 +9
  109 +endobj
  110 +
  111 +%% Original object ID: 12 0
  112 +9 0 obj
  113 +<<
  114 + /Params <<
  115 + /CheckSum <2236c155b1d62b7f00285bba081d4336>
  116 + /Size 11
  117 + /Subtype /text#2fplain
  118 + >>
  119 + /Type /EmbeddedFile
  120 + /Length 10 0 R
  121 +>>
  122 +stream
  123 +from buffer
  124 +endstream
  125 +endobj
  126 +
  127 +%QDF: ignore_newline
  128 +10 0 obj
  129 +11
  130 +endobj
  131 +
  132 +%% Contents for page 1
  133 +%% Original object ID: 14 0
  134 +11 0 obj
  135 +<<
  136 + /Length 12 0 R
  137 +>>
  138 +stream
  139 +BT
  140 + /F1 24 Tf
  141 + 72 720 Td
  142 + (Potato) Tj
  143 +ET
  144 +endstream
  145 +endobj
  146 +
  147 +12 0 obj
  148 +44
  149 +endobj
  150 +
  151 +%% Original object ID: 16 0
  152 +13 0 obj
  153 +<<
  154 + /BaseFont /Helvetica
  155 + /Encoding /WinAnsiEncoding
  156 + /Name /F1
  157 + /Subtype /Type1
  158 + /Type /Font
  159 +>>
  160 +endobj
  161 +
  162 +%% Original object ID: 17 0
  163 +14 0 obj
  164 +[
  165 + /PDF
  166 + /Text
  167 +]
  168 +endobj
  169 +
  170 +xref
  171 +0 15
  172 +0000000000 65535 f
  173 +0000000052 00000 n
  174 +0000000175 00000 n
  175 +0000000281 00000 n
  176 +0000000380 00000 n
  177 +0000000541 00000 n
  178 +0000000708 00000 n
  179 +0000000930 00000 n
  180 +0000001221 00000 n
  181 +0000001267 00000 n
  182 +0000001485 00000 n
  183 +0000001556 00000 n
  184 +0000001657 00000 n
  185 +0000001705 00000 n
  186 +0000001852 00000 n
  187 +trailer <<
  188 + /Root 1 0 R
  189 + /Size 15
  190 + /ID [<31415926535897932384626433832795><31415926535897932384626433832795>]
  191 +>>
  192 +startxref
  193 +1888
  194 +%%EOF
... ...
qpdf/test_driver.cc
... ... @@ -10,6 +10,7 @@
10 10 #include <qpdf/QPDFNameTreeObjectHelper.hh>
11 11 #include <qpdf/QPDFPageLabelDocumentHelper.hh>
12 12 #include <qpdf/QPDFOutlineDocumentHelper.hh>
  13 +#include <qpdf/QPDFEmbeddedFileDocumentHelper.hh>
13 14 #include <qpdf/QUtil.hh>
14 15 #include <qpdf/QTC.hh>
15 16 #include <qpdf/Pl_StdioFile.hh>
... ... @@ -2716,6 +2717,67 @@ void runtest(int n, char const* filename1, char const* arg2)
2716 2717 w.setQDFMode(true);
2717 2718 w.write();
2718 2719 }
  2720 + else if (n == 76)
  2721 + {
  2722 + // Embedded files. arg2 is a file to attach. Hard-code the
  2723 + // mime type and file name for test purposes.
  2724 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  2725 + auto fs1 = QPDFFileSpecObjectHelper::createFileSpec(
  2726 + pdf, "att1.txt", arg2);
  2727 + fs1.setDescription("some text");
  2728 + auto efs1 = QPDFEFStreamObjectHelper(fs1.getEmbeddedFileStream());
  2729 + efs1.setSubtype("text/plain")
  2730 + .setCreationDate("D:20210207191121-05'00'")
  2731 + .setModDate("D:20210208001122Z");
  2732 + efdh.replaceEmbeddedFile("att1", fs1);
  2733 + auto efs2 = QPDFEFStreamObjectHelper::createEFStream(
  2734 + pdf, "from string");
  2735 + efs2.setSubtype("text/plain");
  2736 + Pl_Buffer p("buffer");
  2737 + p.write(QUtil::unsigned_char_pointer("from buffer"), 11);
  2738 + p.finish();
  2739 + auto efs3 = QPDFEFStreamObjectHelper::createEFStream(
  2740 + pdf, p.getBuffer());
  2741 + efs3.setSubtype("text/plain");
  2742 + efdh.replaceEmbeddedFile(
  2743 + "att2", QPDFFileSpecObjectHelper::createFileSpec(
  2744 + pdf, "att2.txt", efs2));
  2745 + auto fs3 = QPDFFileSpecObjectHelper::createFileSpec(
  2746 + pdf, "att3.txt", efs3);
  2747 + efdh.replaceEmbeddedFile("att3", fs3);
  2748 + fs3.setFilename("\xcf\x80.txt", "att3.txt");
  2749 +
  2750 + assert(efs1.getCreationDate() == "D:20210207191121-05'00'");
  2751 + assert(efs1.getModDate() == "D:20210208001122Z");
  2752 + assert(efs2.getSize() == 11);
  2753 + assert(efs2.getSubtype() == "text/plain");
  2754 + assert(QUtil::hex_encode(efs2.getChecksum()) ==
  2755 + "2fce9c8228e360ba9b04a1bd1bf63d6b");
  2756 +
  2757 + for (auto iter: efdh.getEmbeddedFiles())
  2758 + {
  2759 + std::cout << iter.first << " -> " << iter.second->getFilename()
  2760 + << std::endl;
  2761 + }
  2762 + assert(efdh.getEmbeddedFile("att1")->getFilename() == "att1.txt");
  2763 + assert(! efdh.getEmbeddedFile("potato"));
  2764 +
  2765 + QPDFWriter w(pdf, "a.pdf");
  2766 + w.setStaticID(true);
  2767 + w.setQDFMode(true);
  2768 + w.write();
  2769 + }
  2770 + else if (n == 77)
  2771 + {
  2772 + QPDFEmbeddedFileDocumentHelper efdh(pdf);
  2773 + assert(efdh.removeEmbeddedFile("att2"));
  2774 + assert(! efdh.removeEmbeddedFile("att2"));
  2775 +
  2776 + QPDFWriter w(pdf, "a.pdf");
  2777 + w.setStaticID(true);
  2778 + w.setQDFMode(true);
  2779 + w.write();
  2780 + }
2719 2781 else
2720 2782 {
2721 2783 throw std::runtime_error(std::string("invalid test ") +
... ...