UNPKG

22.3 kBJavaScriptView Raw
1const Promise = require(`bluebird`)
2const {
3 GraphQLObjectType,
4 GraphQLList,
5 GraphQLBoolean,
6 GraphQLString,
7 GraphQLInt,
8 GraphQLFloat,
9 GraphQLNonNull,
10 GraphQLJSON,
11} = require(`gatsby/graphql`)
12const {
13 queueImageResizing,
14 base64,
15 fluid,
16 fixed,
17 traceSVG,
18 generateImageData,
19} = require(`gatsby-plugin-sharp`)
20
21const sharp = require(`./safe-sharp`)
22const fs = require(`fs-extra`)
23const imageSize = require(`probe-image-size`)
24const path = require(`path`)
25
26const DEFAULT_PNG_COMPRESSION_SPEED = 4
27
28const {
29 ImageFormatType,
30 ImageCropFocusType,
31 DuotoneGradientType,
32 PotraceTurnPolicyType,
33 PotraceType,
34 ImageFitType,
35 ImageLayoutType,
36 ImagePlaceholderType,
37 JPGOptionsType,
38 PNGOptionsType,
39 WebPOptionsType,
40 BlurredOptionsType,
41 TransformOptionsType,
42} = require(`./types`)
43const { stripIndent } = require(`common-tags`)
44const { prefixId, CODES } = require(`./error-utils`)
45
46function toArray(buf) {
47 const arr = new Array(buf.length)
48
49 for (let i = 0; i < buf.length; i++) {
50 arr[i] = buf[i]
51 }
52
53 return arr
54}
55
56const getTracedSVG = async ({ file, image, fieldArgs, cache, reporter }) =>
57 traceSVG({
58 file,
59 args: { ...fieldArgs.traceSVG },
60 fileArgs: fieldArgs,
61 cache,
62 reporter,
63 })
64
65const fixedNodeType = ({
66 pathPrefix,
67 getNodeAndSavePathDependency,
68 reporter,
69 name,
70 cache,
71}) => {
72 return {
73 type: new GraphQLObjectType({
74 name: name,
75 fields: {
76 base64: { type: GraphQLString },
77 tracedSVG: {
78 type: GraphQLString,
79 resolve: parent =>
80 getTracedSVG({
81 ...parent,
82 cache,
83 reporter,
84 }),
85 },
86 aspectRatio: { type: GraphQLFloat },
87 width: { type: new GraphQLNonNull(GraphQLFloat) },
88 height: { type: new GraphQLNonNull(GraphQLFloat) },
89 src: { type: new GraphQLNonNull(GraphQLString) },
90 srcSet: { type: new GraphQLNonNull(GraphQLString) },
91 srcWebp: {
92 type: GraphQLString,
93 resolve: ({ file, image, fieldArgs }) => {
94 // If the file is already in webp format or should explicitly
95 // be converted to webp, we do not create additional webp files
96 if (file.extension === `webp` || fieldArgs.toFormat === `webp`) {
97 return null
98 }
99 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
100 return Promise.resolve(
101 fixed({
102 file,
103 args,
104 reporter,
105 cache,
106 })
107 ).then(({ src }) => src)
108 },
109 },
110 srcSetWebp: {
111 type: GraphQLString,
112 resolve: ({ file, image, fieldArgs }) => {
113 if (file.extension === `webp` || fieldArgs.toFormat === `webp`) {
114 return null
115 }
116 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
117 return Promise.resolve(
118 fixed({
119 file,
120 args,
121 reporter,
122 cache,
123 })
124 ).then(({ srcSet }) => srcSet)
125 },
126 },
127 originalName: { type: GraphQLString },
128 },
129 }),
130 args: {
131 width: {
132 type: GraphQLInt,
133 },
134 height: {
135 type: GraphQLInt,
136 },
137 base64Width: {
138 type: GraphQLInt,
139 },
140 jpegProgressive: {
141 type: GraphQLBoolean,
142 defaultValue: true,
143 },
144 pngCompressionSpeed: {
145 type: GraphQLInt,
146 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
147 },
148 grayscale: {
149 type: GraphQLBoolean,
150 defaultValue: false,
151 },
152 duotone: {
153 type: DuotoneGradientType,
154 defaultValue: false,
155 },
156 traceSVG: {
157 type: PotraceType,
158 defaultValue: false,
159 },
160 quality: {
161 type: GraphQLInt,
162 },
163 jpegQuality: {
164 type: GraphQLInt,
165 },
166 pngQuality: {
167 type: GraphQLInt,
168 },
169 webpQuality: {
170 type: GraphQLInt,
171 },
172 toFormat: {
173 type: ImageFormatType,
174 defaultValue: ``,
175 },
176 toFormatBase64: {
177 type: ImageFormatType,
178 defaultValue: ``,
179 },
180 cropFocus: {
181 type: ImageCropFocusType,
182 defaultValue: sharp.strategy.attention,
183 },
184 fit: {
185 type: ImageFitType,
186 defaultValue: sharp.fit.cover,
187 },
188 background: {
189 type: GraphQLString,
190 defaultValue: `rgba(0,0,0,1)`,
191 },
192 rotate: {
193 type: GraphQLInt,
194 defaultValue: 0,
195 },
196 trim: {
197 type: GraphQLFloat,
198 defaultValue: false,
199 },
200 },
201 resolve: (image, fieldArgs, context) => {
202 const file = getNodeAndSavePathDependency(image.parent, context.path)
203 const args = { ...fieldArgs, pathPrefix }
204 return Promise.resolve(
205 fixed({
206 file,
207 args,
208 reporter,
209 cache,
210 })
211 ).then(o =>
212 Object.assign({}, o, {
213 fieldArgs: args,
214 image,
215 file,
216 })
217 )
218 },
219 }
220}
221
222const fluidNodeType = ({
223 pathPrefix,
224 getNodeAndSavePathDependency,
225 reporter,
226 name,
227 cache,
228}) => {
229 return {
230 type: new GraphQLObjectType({
231 name: name,
232 fields: {
233 base64: { type: GraphQLString },
234 tracedSVG: {
235 type: GraphQLString,
236 resolve: parent =>
237 getTracedSVG({
238 ...parent,
239 cache,
240 reporter,
241 }),
242 },
243 aspectRatio: { type: new GraphQLNonNull(GraphQLFloat) },
244 src: { type: new GraphQLNonNull(GraphQLString) },
245 srcSet: { type: new GraphQLNonNull(GraphQLString) },
246 srcWebp: {
247 type: GraphQLString,
248 resolve: ({ file, image, fieldArgs }) => {
249 if (image.extension === `webp` || fieldArgs.toFormat === `webp`) {
250 return null
251 }
252 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
253 return Promise.resolve(
254 fluid({
255 file,
256 args,
257 reporter,
258 cache,
259 })
260 ).then(({ src }) => src)
261 },
262 },
263 srcSetWebp: {
264 type: GraphQLString,
265 resolve: ({ file, image, fieldArgs }) => {
266 if (image.extension === `webp` || fieldArgs.toFormat === `webp`) {
267 return null
268 }
269 const args = { ...fieldArgs, pathPrefix, toFormat: `webp` }
270 return Promise.resolve(
271 fluid({
272 file,
273 args,
274 reporter,
275 cache,
276 })
277 ).then(({ srcSet }) => srcSet)
278 },
279 },
280 sizes: { type: new GraphQLNonNull(GraphQLString) },
281 originalImg: { type: GraphQLString },
282 originalName: { type: GraphQLString },
283 presentationWidth: { type: new GraphQLNonNull(GraphQLInt) },
284 presentationHeight: { type: new GraphQLNonNull(GraphQLInt) },
285 },
286 }),
287 args: {
288 maxWidth: {
289 type: GraphQLInt,
290 },
291 maxHeight: {
292 type: GraphQLInt,
293 },
294 base64Width: {
295 type: GraphQLInt,
296 },
297 grayscale: {
298 type: GraphQLBoolean,
299 defaultValue: false,
300 },
301 jpegProgressive: {
302 type: GraphQLBoolean,
303 defaultValue: true,
304 },
305 pngCompressionSpeed: {
306 type: GraphQLInt,
307 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
308 },
309 duotone: {
310 type: DuotoneGradientType,
311 defaultValue: false,
312 },
313 traceSVG: {
314 type: PotraceType,
315 defaultValue: false,
316 },
317 quality: {
318 type: GraphQLInt,
319 },
320 jpegQuality: {
321 type: GraphQLInt,
322 },
323 pngQuality: {
324 type: GraphQLInt,
325 },
326 webpQuality: {
327 type: GraphQLInt,
328 },
329 toFormat: {
330 type: ImageFormatType,
331 defaultValue: ``,
332 },
333 toFormatBase64: {
334 type: ImageFormatType,
335 defaultValue: ``,
336 },
337 cropFocus: {
338 type: ImageCropFocusType,
339 defaultValue: sharp.strategy.attention,
340 },
341 fit: {
342 type: ImageFitType,
343 defaultValue: sharp.fit.cover,
344 },
345 background: {
346 type: GraphQLString,
347 defaultValue: `rgba(0,0,0,1)`,
348 },
349 rotate: {
350 type: GraphQLInt,
351 defaultValue: 0,
352 },
353 trim: {
354 type: GraphQLFloat,
355 defaultValue: false,
356 },
357 sizes: {
358 type: GraphQLString,
359 defaultValue: ``,
360 },
361 srcSetBreakpoints: {
362 type: GraphQLList(GraphQLInt),
363 defaultValue: [],
364 description: `A list of image widths to be generated. Example: [ 200, 340, 520, 890 ]`,
365 },
366 },
367 resolve: (image, fieldArgs, context) => {
368 const file = getNodeAndSavePathDependency(image.parent, context.path)
369 const args = { ...fieldArgs, pathPrefix }
370 return Promise.resolve(
371 fluid({
372 file,
373 args,
374 reporter,
375 cache,
376 })
377 ).then(o =>
378 Object.assign({}, o, {
379 fieldArgs: args,
380 image,
381 file,
382 })
383 )
384 },
385 }
386}
387
388let warnedForAlpha = false
389
390const imageNodeType = ({
391 pathPrefix,
392 getNodeAndSavePathDependency,
393 reporter,
394 cache,
395}) => {
396 return {
397 type: new GraphQLNonNull(GraphQLJSON),
398 args: {
399 layout: {
400 type: ImageLayoutType,
401 defaultValue: `fixed`,
402 description: stripIndent`
403 The layout for the image.
404 FIXED: A static image sized, that does not resize according to the screen width
405 FLUID: The image resizes to fit its container. Pass a "sizes" option if it isn't going to be the full width of the screen.
406 CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.
407 `,
408 },
409 maxWidth: {
410 type: GraphQLInt,
411 description: stripIndent`
412 Maximum display width of generated files.
413 The actual largest image resolution will be this value multipled by the largest value in outputPixelDensities
414 This only applies when layout = FLUID or CONSTRAINED. For other layout types, use "width"`,
415 },
416 maxHeight: {
417 type: GraphQLInt,
418 description: stripIndent`
419 If set, the generated image is a maximum of this height, cropping if necessary.
420 If the image layout is "constrained" then the image will be limited to this height.
421 If the aspect ratio of the image is different than the source, then the image will be cropped.`,
422 },
423 width: {
424 type: GraphQLInt,
425 description: stripIndent`
426 The display width of the generated image.
427 The actual largest image resolution will be this value multipled by the largest value in outputPixelDensities
428 Ignored if layout = FLUID or CONSTRAINED, where you should use "maxWidth" instead.
429 `,
430 },
431 height: {
432 type: GraphQLInt,
433 description: stripIndent`
434 If set, the height of the generated image. If omitted, it is calculated from the supplied width, matching the aspect ratio of the source image.`,
435 },
436 placeholder: {
437 type: ImagePlaceholderType,
438 defaultValue: `blurred`,
439 description: stripIndent`
440 Format of generated placeholder image, displayed while the main image loads.
441 BLURRED: a blurred, low resolution image, encoded as a base64 data URI (default)
442 DOMINANT_COLOR: a solid color, calculated from the dominant color of the image.
443 TRACED_SVG: a low-resolution traced SVG of the image.
444 NONE: no placeholder. Set "background" to use a fixed background color.`,
445 },
446 blurredOptions: {
447 type: BlurredOptionsType,
448 description: `Options for the low-resolution placeholder image. Set placeholder to "BLURRED" to use this`,
449 },
450 tracedSVGOptions: {
451 type: PotraceType,
452 defaultValue: false,
453 description: `Options for traced placeholder SVGs. You also should set placeholder to "SVG".`,
454 },
455 formats: {
456 type: GraphQLList(ImageFormatType),
457 description: stripIndent`
458 The image formats to generate. Valid values are "AUTO" (meaning the same format as the source image), "JPG", "PNG" and "WEBP".
459 The default value is [AUTO, WEBP], and you should rarely need to change this. Take care if you specify JPG or PNG when you do
460 not know the formats of the source images, as this could lead to unwanted results such as converting JPEGs to PNGs. Specifying
461 both PNG and JPG is not supported and will be ignored.
462 `,
463 defaultValue: [`auto`, `webp`],
464 },
465 outputPixelDensities: {
466 type: GraphQLList(GraphQLFloat),
467 description: stripIndent`
468 A list of image pixel densities to generate. It will never generate images larger than the source, and will always include a 1x image.
469 Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, 3x, and [0.25, 0.5, 1, 2] for fluid. In this case, an image with a fluid layout and width = 400 would generate images at 100, 200, 400 and 800px wide`,
470 },
471 sizes: {
472 type: GraphQLString,
473 defaultValue: ``,
474 description: stripIndent`
475 The "sizes" property, passed to the img tag. This describes the display size of the image.
476 This does not affect the generated images, but is used by the browser to decide which images to download. You can leave this blank for fixed images, or if the responsive image
477 container will be the full width of the screen. In these cases we will generate an appropriate value.
478 `,
479 },
480 quality: {
481 type: GraphQLInt,
482 description: `The default quality. This is overriden by any format-specific options`,
483 },
484 jpgOptions: {
485 type: JPGOptionsType,
486 description: `Options to pass to sharp when generating JPG images.`,
487 },
488 pngOptions: {
489 type: PNGOptionsType,
490 description: `Options to pass to sharp when generating PNG images.`,
491 },
492 webpOptions: {
493 type: WebPOptionsType,
494 description: `Options to pass to sharp when generating WebP images.`,
495 },
496 transformOptions: {
497 type: TransformOptionsType,
498 description: `Options to pass to sharp to control cropping and other image manipulations.`,
499 },
500 background: {
501 type: GraphQLString,
502 defaultValue: `rgba(0,0,0,0)`,
503 description: `Background color applied to the wrapper. Also passed to sharp to use as a background when "letterboxing" an image to another aspect ratio.`,
504 },
505 },
506 resolve: async (image, fieldArgs, context) => {
507 const file = getNodeAndSavePathDependency(image.parent, context.path)
508 const args = { ...fieldArgs, pathPrefix }
509
510 if (!generateImageData) {
511 reporter.warn(`Please upgrade gatsby-plugin-sharp`)
512 return null
513 }
514 if (!warnedForAlpha) {
515 reporter.warn(
516 stripIndent`
517 You are using the alpha version of the \`gatsbyImageData\` sharp API, which is unstable and will change without notice.
518 Please do not use it in production.`
519 )
520 warnedForAlpha = true
521 }
522 const imageData = await generateImageData({
523 file,
524 args,
525 reporter,
526 cache,
527 })
528
529 return imageData
530 },
531 }
532}
533
534/**
535 * Keeps track of asynchronous file copy to prevent sequence errors in the
536 * underlying fs-extra module during parallel copies of the same file
537 */
538const inProgressCopy = new Set()
539
540const createFields = ({
541 pathPrefix,
542 getNodeAndSavePathDependency,
543 reporter,
544 cache,
545}) => {
546 const nodeOptions = {
547 pathPrefix,
548 getNodeAndSavePathDependency,
549 reporter,
550 cache,
551 }
552
553 // TODO: Remove resolutionsNode and sizesNode for Gatsby v3
554 const fixedNode = fixedNodeType({ name: `ImageSharpFixed`, ...nodeOptions })
555 const resolutionsNode = fixedNodeType({
556 name: `ImageSharpResolutions`,
557 ...nodeOptions,
558 })
559 resolutionsNode.deprecationReason = `Resolutions was deprecated in Gatsby v2. It's been renamed to "fixed" https://example.com/write-docs-and-fix-this-example-link`
560
561 const fluidNode = fluidNodeType({ name: `ImageSharpFluid`, ...nodeOptions })
562 const sizesNode = fluidNodeType({ name: `ImageSharpSizes`, ...nodeOptions })
563 sizesNode.deprecationReason = `Sizes was deprecated in Gatsby v2. It's been renamed to "fluid" https://example.com/write-docs-and-fix-this-example-link`
564
565 const imageNode = imageNodeType(nodeOptions)
566
567 return {
568 fixed: fixedNode,
569 resolutions: resolutionsNode,
570 fluid: fluidNode,
571 sizes: sizesNode,
572 gatsbyImageData: imageNode,
573 original: {
574 type: new GraphQLObjectType({
575 name: `ImageSharpOriginal`,
576 fields: {
577 width: { type: GraphQLFloat },
578 height: { type: GraphQLFloat },
579 src: { type: GraphQLString },
580 },
581 }),
582 args: {},
583 async resolve(image, fieldArgs, context) {
584 const details = getNodeAndSavePathDependency(image.parent, context.path)
585 const dimensions = imageSize.sync(
586 toArray(fs.readFileSync(details.absolutePath))
587 )
588 const imageName = `${details.name}-${image.internal.contentDigest}${details.ext}`
589 const publicPath = path.join(
590 process.cwd(),
591 `public`,
592 `static`,
593 imageName
594 )
595
596 if (!fs.existsSync(publicPath) && !inProgressCopy.has(publicPath)) {
597 // keep track of in progress copy, we should rely on `existsSync` but
598 // a race condition exists between the exists check and the copy
599 inProgressCopy.add(publicPath)
600 fs.copy(details.absolutePath, publicPath, err => {
601 // this is no longer in progress
602 inProgressCopy.delete(publicPath)
603 if (err) {
604 reporter.panic(
605 {
606 id: prefixId(CODES.MissingResource),
607 context: {
608 sourceMessage: `error copying file from ${details.absolutePath} to ${publicPath}`,
609 },
610 },
611 err
612 )
613 }
614 })
615 }
616
617 return {
618 width: dimensions.width,
619 height: dimensions.height,
620 src: `${pathPrefix}/static/${imageName}`,
621 }
622 },
623 },
624 resize: {
625 type: new GraphQLObjectType({
626 name: `ImageSharpResize`,
627 fields: {
628 src: { type: GraphQLString },
629 tracedSVG: {
630 type: GraphQLString,
631 resolve: parent =>
632 getTracedSVG({
633 ...parent,
634 cache,
635 reporter,
636 }),
637 },
638 width: { type: GraphQLInt },
639 height: { type: GraphQLInt },
640 aspectRatio: { type: GraphQLFloat },
641 originalName: { type: GraphQLString },
642 },
643 }),
644 args: {
645 width: {
646 type: GraphQLInt,
647 },
648 height: {
649 type: GraphQLInt,
650 },
651 quality: {
652 type: GraphQLInt,
653 },
654 jpegQuality: {
655 type: GraphQLInt,
656 },
657 pngQuality: {
658 type: GraphQLInt,
659 },
660 webpQuality: {
661 type: GraphQLInt,
662 },
663 jpegProgressive: {
664 type: GraphQLBoolean,
665 defaultValue: true,
666 },
667 pngCompressionLevel: {
668 type: GraphQLInt,
669 defaultValue: 9,
670 },
671 pngCompressionSpeed: {
672 type: GraphQLInt,
673 defaultValue: DEFAULT_PNG_COMPRESSION_SPEED,
674 },
675 grayscale: {
676 type: GraphQLBoolean,
677 defaultValue: false,
678 },
679 duotone: {
680 type: DuotoneGradientType,
681 defaultValue: false,
682 },
683 base64: {
684 type: GraphQLBoolean,
685 defaultValue: false,
686 },
687 traceSVG: {
688 type: PotraceType,
689 defaultValue: false,
690 },
691 toFormat: {
692 type: ImageFormatType,
693 defaultValue: ``,
694 },
695 cropFocus: {
696 type: ImageCropFocusType,
697 defaultValue: sharp.strategy.attention,
698 },
699 fit: {
700 type: ImageFitType,
701 defaultValue: sharp.fit.cover,
702 },
703 background: {
704 type: GraphQLString,
705 defaultValue: `rgba(0,0,0,1)`,
706 },
707 rotate: {
708 type: GraphQLInt,
709 defaultValue: 0,
710 },
711 trim: {
712 type: GraphQLFloat,
713 defaultValue: 0,
714 },
715 },
716 resolve: (image, fieldArgs, context) => {
717 const file = getNodeAndSavePathDependency(image.parent, context.path)
718 const args = { ...fieldArgs, pathPrefix }
719 return new Promise(resolve => {
720 if (fieldArgs.base64) {
721 resolve(
722 base64({
723 file,
724 cache,
725 })
726 )
727 } else {
728 const o = queueImageResizing({
729 file,
730 args,
731 })
732 resolve(
733 Object.assign({}, o, {
734 image,
735 file,
736 fieldArgs: args,
737 })
738 )
739 }
740 })
741 },
742 },
743 }
744}
745
746module.exports = ({
747 actions,
748 schema,
749 pathPrefix,
750 getNodeAndSavePathDependency,
751 reporter,
752 cache,
753}) => {
754 const { createTypes } = actions
755
756 const imageSharpType = schema.buildObjectType({
757 name: `ImageSharp`,
758 fields: createFields({
759 pathPrefix,
760 getNodeAndSavePathDependency,
761 reporter,
762 cache,
763 }),
764 interfaces: [`Node`],
765 extensions: {
766 infer: true,
767 childOf: {
768 types: [`File`],
769 },
770 },
771 })
772
773 if (createTypes) {
774 createTypes([
775 ImageFormatType,
776 ImageFitType,
777 ImageLayoutType,
778 ImageCropFocusType,
779 DuotoneGradientType,
780 PotraceTurnPolicyType,
781 PotraceType,
782 imageSharpType,
783 ])
784 }
785}