Coverage Report

Created: 2026-11-12 07:22

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/jpegoptim/stripai.c
Line
Count
Source
1
/*
2
 * stripai.c
3
 *
4
 * Copyright (C) 2026 Matteo Croce
5
 * All Rights Reserved.
6
 *
7
 * Removal of the metadata telling that an image was generated or edited
8
 * with AI: C2PA manifests (Content Credentials), the IPTC digital source
9
 * type in XMP, and the generation parameters (prompt, seed, sampler...)
10
 * that Stable Diffusion UIs store in the Exif UserComment.
11
 *
12
 * SPDX-License-Identifier: GPL-3.0-or-later
13
 *
14
 * This file is part of JPEGoptim.
15
 *
16
 * JPEGoptim is free software: you can redistribute it and/or modify
17
 * it under the terms of the GNU General Public License as published by
18
 * the Free Software Foundation, either version 3 of the License, or
19
 * (at your option) any later version.
20
 *
21
 * JPEGoptim is distributed in the hope that it will be useful,
22
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
23
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
 * GNU General Public License for more details.
25
 *
26
 * You should have received a copy of the GNU General Public License
27
 * along with JPEGoptim. If not, see <https://www.gnu.org/licenses/>.
28
 */
29
30
#include <stdio.h>
31
#include <stdlib.h>
32
#include <string.h>
33
#include <ctype.h>
34
#include <jpeglib.h>
35
36
#include "jpegoptim.h"
37
38
0
#define XMP_ID "http://ns.adobe.com/xap/1.0/"
39
0
#define XMP_ID_LEN (sizeof(XMP_ID))  /* including the NUL terminator */
40
0
#define EXIF_ID_LEN 6      /* "Exif\0\0" */
41
42
0
#define EXIF_IFD_POINTER 0x8769
43
0
#define EXIF_USER_COMMENT 0x9286
44
45
46
/* Algorithmic IPTC digital source types: trainedAlgorithmicMedia,
47
   compositeWithTrainedAlgorithmicMedia, algorithmicMedia, ... */
48
static const char *ai_source_types[] = {
49
  "algorithmicmedia",
50
  "compositesynthetic",
51
  "algorithmicallyenhanced",
52
  NULL
53
};
54
55
/* References to a C2PA manifest (dcterms:provenance) */
56
static const char *c2pa_refs[] = {
57
  "c2pa",
58
  NULL
59
};
60
61
/* Generation parameters, as written by AUTOMATIC1111, Forge and SD.Next */
62
static const char *ai_parameters[] = {
63
  "negative prompt:",
64
  "cfg scale:",
65
  "model hash:",
66
  NULL
67
};
68
69
70
/* Case insensitive search of any of the strings in buf. NUL bytes in buf
71
   are skipped, so that UTF-16 text matches as well. */
72
static int has_any(const unsigned char *buf, size_t len, const char **strs)
73
0
{
74
0
  for (; *strs; strs++) {
75
0
    for (size_t i = 0; i < len; i++) {
76
0
      const char *s = *strs;
77
0
      size_t j = i;
78
79
0
      if (buf[i] == 0)
80
0
        continue;
81
82
0
      while (*s && j < len) {
83
0
        if (buf[j] == 0) {
84
0
          j++;
85
0
          continue;
86
0
        }
87
0
        if (tolower(buf[j]) != *s)
88
0
          break;
89
0
        s++;
90
0
        j++;
91
0
      }
92
0
      if (!*s)
93
0
        return 1;
94
0
    }
95
0
  }
96
97
0
  return 0;
98
0
}
99
100
101
static long find(const unsigned char *buf, size_t len, size_t pos,
102
    const char *str, size_t str_len)
103
0
{
104
0
  for (; pos + str_len <= len; pos++) {
105
0
    if (!memcmp(buf + pos, str, str_len))
106
0
      return pos;
107
0
  }
108
109
0
  return -1;
110
0
}
111
112
113
static int is_name_char(int c)
114
0
{
115
0
  return (isalnum(c) || c == '_' || c == '-' || c == '.');
116
0
}
117
118
119
/* Remove from an XMP packet the properties having the given local name
120
   and a value containing any of the given strings. Both the attribute
121
   (prefix:name="value") and the element (<prefix:name>...</prefix:name>)
122
   forms are handled. Returns the number of properties removed. */
123
static int xmp_remove_property(unsigned char *buf, size_t *len,
124
      const char *name, const char **values)
125
0
{
126
0
  size_t name_len = strlen(name);
127
0
  size_t pos = 0;
128
0
  int count = 0;
129
0
  long i;
130
131
0
  while ((i = find(buf, *len, pos, name, name_len)) >= 0) {
132
0
    size_t qname, start, end, value;
133
134
0
    pos = value = i + name_len;
135
136
    /* the name must be prefixed by a namespace: "prefix:name" */
137
0
    if (i < 2 || buf[i - 1] != ':')
138
0
      continue;
139
0
    qname = i - 1;
140
0
    while (qname > 0 && is_name_char(buf[qname - 1]))
141
0
      qname--;
142
0
    if (qname == 0 || qname == (size_t)i - 1)
143
0
      continue;
144
145
0
    if (buf[qname - 1] == '<') {
146
      /* element: <prefix:name .../> or <prefix:name ...>...</prefix:name> */
147
0
      long gt = find(buf, *len, pos, ">", 1);
148
149
0
      if (gt < 0)
150
0
        continue;
151
0
      start = qname - 1;
152
0
      end = gt + 1;
153
0
      if (buf[gt - 1] != '/') {
154
0
        size_t qname_len = value - qname;
155
0
        long close = gt;
156
157
0
        while ((close = find(buf, *len, close + 1, "</", 2)) >= 0) {
158
0
          if (close + 2 + qname_len < *len
159
0
              && !memcmp(buf + close + 2, buf + qname, qname_len)
160
0
              && !is_name_char(buf[close + 2 + qname_len]))
161
0
            break;
162
0
        }
163
0
        if (close < 0 || (gt = find(buf, *len, close, ">", 1)) < 0)
164
0
          continue;
165
0
        end = gt + 1;
166
0
      }
167
0
    } else if (isspace(buf[qname - 1])) {
168
      /* attribute: prefix:name="value", removed with its leading space */
169
0
      size_t p = pos;
170
0
      long quote;
171
172
0
      while (p < *len && isspace(buf[p]))
173
0
        p++;
174
0
      if (p >= *len || buf[p++] != '=')
175
0
        continue;
176
0
      while (p < *len && isspace(buf[p]))
177
0
        p++;
178
0
      if (p >= *len || (buf[p] != '"' && buf[p] != '\''))
179
0
        continue;
180
0
      if ((quote = find(buf, *len, p + 1, (char *)buf + p, 1)) < 0)
181
0
        continue;
182
0
      start = qname - 1;
183
0
      end = quote + 1;
184
0
    } else {
185
0
      continue;
186
0
    }
187
188
0
    if (!has_any(buf + value, end - value, values))
189
0
      continue;
190
191
0
    memmove(buf + start, buf + end, *len - end);
192
0
    *len -= end - start;
193
0
    pos = start;
194
0
    count++;
195
0
  }
196
197
0
  return count;
198
0
}
199
200
201
static unsigned int get16(const unsigned char *p, int le)
202
0
{
203
0
  return (le ? p[0] | p[1] << 8 : p[0] << 8 | p[1]);
204
0
}
205
206
207
static unsigned long get32(const unsigned char *p, int le)
208
0
{
209
0
  return (le ? get16(p, le) | (unsigned long)get16(p + 2, le) << 16
210
0
    : (unsigned long)get16(p, le) << 16 | get16(p + 2, le));
211
0
}
212
213
214
/* Look for a tag in a TIFF IFD, returning the offset of its entry,
215
   or 0 if not found. */
216
static size_t ifd_find_tag(const unsigned char *tiff, size_t len, int le,
217
      size_t ifd, unsigned int tag)
218
0
{
219
0
  unsigned int n;
220
221
0
  if (ifd < 8 || ifd >= len || len - ifd < 2)
222
0
    return 0;
223
0
  n = get16(tiff + ifd, le);
224
0
  for (unsigned int i = 0; i < n; i++) {
225
0
    size_t entry = ifd + 2 + i * 12;
226
227
0
    if (entry + 12 > len)
228
0
      break;
229
0
    if (get16(tiff + entry, le) == tag)
230
0
      return entry;
231
0
  }
232
233
0
  return 0;
234
0
}
235
236
237
/* Blank in place the Exif UserComment, if it holds generation parameters.
238
   It is not removed, so that no offset in the Exif data changes. */
239
static int exif_clear_user_comment(unsigned char *data, size_t len)
240
0
{
241
0
  unsigned char *tiff = data + EXIF_ID_LEN;
242
0
  size_t entry, offset, count;
243
0
  int le;
244
245
0
  if (len < EXIF_ID_LEN + 8)
246
0
    return 0;
247
0
  len -= EXIF_ID_LEN;
248
0
  if (!memcmp(tiff, "II", 2))
249
0
    le = 1;
250
0
  else if (!memcmp(tiff, "MM", 2))
251
0
    le = 0;
252
0
  else
253
0
    return 0;
254
255
0
  if (!(entry = ifd_find_tag(tiff, len, le, get32(tiff + 4, le), EXIF_IFD_POINTER)))
256
0
    return 0;
257
0
  if (!(entry = ifd_find_tag(tiff, len, le, get32(tiff + entry + 8, le), EXIF_USER_COMMENT)))
258
0
    return 0;
259
260
  /* 8 bytes of character code, then the comment: as the UserComment
261
     is of UNDEFINED type, its size is the count itself */
262
0
  count = get32(tiff + entry + 4, le);
263
0
  if (count <= 8)
264
0
    return 0;
265
0
  offset = get32(tiff + entry + 8, le);
266
0
  if (offset > len || count > len - offset)
267
0
    return 0;
268
0
  if (!has_any(tiff + offset + 8, count - 8, ai_parameters))
269
0
    return 0;
270
271
0
  memset(tiff + offset + 8, 0, count - 8);
272
273
0
  return 1;
274
0
}
275
276
277
/* Remove the AI metadata from the markers saved by the decompressor.
278
   Returns the number of items removed. */
279
int strip_ai_markers(struct jpeg_decompress_struct *dinfo)
280
0
{
281
0
  jpeg_saved_marker_ptr *mp = &dinfo->marker_list;
282
0
  long c2pa_box = -1;
283
0
  int count = 0;
284
285
0
  while (*mp) {
286
0
    jpeg_saved_marker_ptr m = *mp;
287
288
0
    if (m->marker == JPEG_APP0 + 11 && m->data_length >= 8
289
0
        && !memcmp(m->data, "JP", 2)) {
290
      /* JUMBF: "JP", box instance and sequence number, then the
291
         superbox, which may continue in other APP11 markers of the
292
         same box instance. The first one has the description box
293
         telling the C2PA content type. */
294
0
      long box = get16(m->data + 2, 0);
295
296
0
      if (m->data_length >= 28 && !memcmp(m->data + 12, "jumb", 4)
297
0
          && !memcmp(m->data + 20, "jumdc2pa", 8))
298
0
        c2pa_box = box;
299
0
      if (box == c2pa_box) {
300
0
        *mp = m->next;
301
0
        count++;
302
0
        continue;
303
0
      }
304
0
    } else if (m->marker == JPEG_APP0 + 1 && m->data_length > XMP_ID_LEN
305
0
         && !memcmp(m->data, XMP_ID, XMP_ID_LEN)) {
306
0
      size_t len = m->data_length - XMP_ID_LEN;
307
0
      int n;
308
309
0
      n = xmp_remove_property(m->data + XMP_ID_LEN, &len,
310
0
            "DigitalSourceType", ai_source_types);
311
0
      n += xmp_remove_property(m->data + XMP_ID_LEN, &len,
312
0
            "provenance", c2pa_refs);
313
0
      m->data_length = XMP_ID_LEN + len;
314
0
      count += n;
315
0
    } else if (m->marker == JPEG_APP0 + 1 && m->data_length > EXIF_ID_LEN
316
0
         && !memcmp(m->data, "Exif\0\0", EXIF_ID_LEN)) {
317
0
      count += exif_clear_user_comment(m->data, m->data_length);
318
0
    }
319
320
0
    mp = &m->next;
321
0
  }
322
323
0
  return count;
324
0
}